chatsecureios / ChatSecure / Classes / View Controllers / OTRMessagesViewController.m @ f3813541
History | View | Annotate | Download (96.7 KB)
| 1 | // | 
|---|---|
| 2 | // OTRMessagesViewController.m | 
| 3 | // Off the Record | 
| 4 | // | 
| 5 | // Created by David Chiles on 5/12/14. | 
| 6 | // Copyright (c) 2014 Chris Ballinger. All rights reserved. | 
| 7 | // | 
| 8 |  | 
| 9 | #import "OTRMessagesViewController.h" | 
| 10 |  | 
| 11 | #import "OTRDatabaseView.h" | 
| 12 | #import "OTRDatabaseManager.h" | 
| 13 | #import "OTRLog.h" | 
| 14 |  | 
| 15 | #import "OTRBuddy.h" | 
| 16 | #import "OTRAccount.h" | 
| 17 | #import "OTRMessage+JSQMessageData.h" | 
| 18 | @import JSQMessagesViewController; | 
| 19 | @import MobileCoreServices; | 
| 20 | #import "OTRProtocolManager.h" | 
| 21 | #import "OTRXMPPTorAccount.h" | 
| 22 | #import "OTRXMPPManager.h" | 
| 23 | #import "OTRLockButton.h" | 
| 24 | #import "OTRButtonView.h" | 
| 25 | @import OTRAssets; | 
| 26 | #import "OTRTitleSubtitleView.h" | 
| 27 | @import OTRKit; | 
| 28 | @import FormatterKit; | 
| 29 | #import "OTRImages.h" | 
| 30 | #import "UIActivityViewController+ChatSecure.h" | 
| 31 | #import "OTRUtilities.h" | 
| 32 | #import "OTRProtocolManager.h" | 
| 33 | #import "OTRColors.h" | 
| 34 | #import "JSQMessagesCollectionViewCell+ChatSecure.h" | 
| 35 | @import BButton; | 
| 36 | #import "OTRAttachmentPicker.h" | 
| 37 | #import "OTRImageItem.h" | 
| 38 | #import "OTRVideoItem.h" | 
| 39 | #import "OTRAudioItem.h" | 
| 40 | @import JTSImageViewController; | 
| 41 | #import "OTRAudioControlsView.h" | 
| 42 | #import "OTRPlayPauseProgressView.h" | 
| 43 | #import "OTRAudioPlaybackController.h" | 
| 44 | #import "OTRMediaFileManager.h" | 
| 45 | #import "OTRMediaServer.h" | 
| 46 | #import "UIImage+ChatSecure.h" | 
| 47 | #import "OTRBaseLoginViewController.h" | 
| 48 |  | 
| 49 | #import <ChatSecureCore/ChatSecureCore-Swift.h> | 
| 50 | #import "OTRYapMessageSendAction.h" | 
| 51 | #import "UIViewController+ChatSecure.h" | 
| 52 | #import "OTRBuddyCache.h" | 
| 53 | #import "OTRTextItem.h" | 
| 54 | #import "OTRHTMLItem.h" | 
| 55 | #import "OTRFileItem.h" | 
| 56 | @import YapDatabase; | 
| 57 | @import PureLayout; | 
| 58 | @import KVOController; | 
| 59 |  | 
| 60 | @import AVFoundation; | 
| 61 | @import MediaPlayer; | 
| 62 |  | 
| 63 | static NSTimeInterval const kOTRMessageSentDateShowTimeInterval = 5 * 60; | 
| 64 | static NSUInteger const kOTRMessagePageSize = 50; | 
| 65 |  | 
| 66 | typedef NS_ENUM(int, OTRDropDownType) {
 | 
| 67 | OTRDropDownTypeNone = 0, | 
| 68 | OTRDropDownTypeEncryption = 1, | 
| 69 | OTRDropDownTypePush = 2 | 
| 70 | }; | 
| 71 |  | 
| 72 | @interface OTRMessagesViewController () <UITextViewDelegate, OTRAttachmentPickerDelegate, OTRYapViewHandlerDelegateProtocol, OTRMessagesCollectionViewFlowLayoutSizeProtocol, OTRRoomOccupantsViewControllerDelegate> {
 | 
| 73 | JSQMessagesAvatarImage *_warningAvatarImage; | 
| 74 | JSQMessagesAvatarImage *_accountAvatarImage; | 
| 75 | JSQMessagesAvatarImage *_buddyAvatarImage; | 
| 76 | } | 
| 77 |  | 
| 78 | @property (nonatomic, strong) OTRYapViewHandler *viewHandler; | 
| 79 |  | 
| 80 | @property (nonatomic, strong) JSQMessagesBubbleImage *outgoingBubbleImage; | 
| 81 | @property (nonatomic, strong) JSQMessagesBubbleImage *incomingBubbleImage; | 
| 82 |  | 
| 83 | @property (nonatomic, weak) id didFinishGeneratingPrivateKeyNotificationObject; | 
| 84 | @property (nonatomic, weak) id messageStateDidChangeNotificationObject; | 
| 85 | @property (nonatomic, weak) id pendingApprovalDidChangeNotificationObject; | 
| 86 | @property (nonatomic, weak) id deviceListUpdateNotificationObject; | 
| 87 |  | 
| 88 |  | 
| 89 | @property (nonatomic ,strong) UIBarButtonItem *lockBarButtonItem; | 
| 90 | @property (nonatomic, strong) OTRLockButton *lockButton; | 
| 91 | @property (nonatomic, strong) OTRButtonView *buttonDropdownView; | 
| 92 |  | 
| 93 | @property (nonatomic, strong) OTRAttachmentPicker *attachmentPicker; | 
| 94 | @property (nonatomic, strong) OTRAudioPlaybackController *audioPlaybackController; | 
| 95 |  | 
| 96 | @property (nonatomic, strong) NSTimer *lastSeenRefreshTimer; | 
| 97 | @property (nonatomic, strong) UIView *jidForwardingHeaderView; | 
| 98 |  | 
| 99 | @property (nonatomic) BOOL loadingMessages; | 
| 100 | @property (nonatomic) BOOL messageRangeExtended; | 
| 101 | @property (nonatomic, strong) NSIndexPath *currentIndexPath; | 
| 102 | @property (nonatomic, strong) id currentMessage; | 
| 103 | @property (nonatomic, strong) NSCache *messageSizeCache; | 
| 104 |  | 
| 105 | @end | 
| 106 |  | 
| 107 | @implementation OTRMessagesViewController | 
| 108 |  | 
| 109 | - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil | 
| 110 | {
 | 
| 111 |     if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) {
 | 
| 112 | self.senderId = @""; | 
| 113 | self.senderDisplayName = @""; | 
| 114 | _state = [[MessagesViewControllerState alloc] init]; | 
| 115 | self.messageSizeCache = [NSCache new]; | 
| 116 | self.messageSizeCache.countLimit = kOTRMessagePageSize; | 
| 117 | self.messageRangeExtended = NO; | 
| 118 | } | 
| 119 | return self; | 
| 120 | } | 
| 121 |  | 
| 122 | #pragma - mark Lifecylce Methods | 
| 123 |  | 
| 124 | - (void) dealloc {
 | 
| 125 | [self.lastSeenRefreshTimer invalidate]; | 
| 126 | [[NSNotificationCenter defaultCenter] removeObserver:self]; | 
| 127 | } | 
| 128 |  | 
| 129 | - (void)viewDidLoad | 
| 130 | {
 | 
| 131 | [super viewDidLoad]; | 
| 132 |  | 
| 133 | self.automaticallyScrollsToMostRecentMessage = YES; | 
| 134 |  | 
| 135 | ////// bubbles ////// | 
| 136 | JSQMessagesBubbleImageFactory *bubbleImageFactory = [[JSQMessagesBubbleImageFactory alloc] init]; | 
| 137 |  | 
| 138 | self.outgoingBubbleImage = [bubbleImageFactory outgoingMessagesBubbleImageWithColor:[UIColor jsq_messageBubbleBlueColor]]; | 
| 139 |  | 
| 140 | self.incomingBubbleImage = [bubbleImageFactory incomingMessagesBubbleImageWithColor:[UIColor jsq_messageBubbleLightGrayColor]]; | 
| 141 |  | 
| 142 | ////// TitleView ////// | 
| 143 | OTRTitleSubtitleView *titleView = [self titleView]; | 
| 144 | [self refreshTitleView:titleView]; | 
| 145 | self.navigationItem.titleView = titleView; | 
| 146 |  | 
| 147 | ////// Send Button ////// | 
| 148 | self.sendButton = [JSQMessagesToolbarButtonFactory defaultSendButtonItem]; | 
| 149 |  | 
| 150 | ////// Attachment Button ////// | 
| 151 | self.inputToolbar.contentView.leftBarButtonItem = nil; | 
| 152 | self.cameraButton = [UIButton buttonWithType:UIButtonTypeRoundedRect]; | 
| 153 | self.cameraButton.titleLabel.font = [UIFont fontWithName:kFontAwesomeFont size:20]; | 
| 154 | self.cameraButton.titleLabel.textAlignment = NSTextAlignmentCenter; | 
| 155 | [self.cameraButton setTitle:[NSString fa_stringForFontAwesomeIcon:FACamera] forState:UIControlStateNormal]; | 
| 156 | self.cameraButton.frame = CGRectMake(0, 0, 32, 32); | 
| 157 |  | 
| 158 | ////// Microphone Button ////// | 
| 159 | self.microphoneButton = [UIButton buttonWithType:UIButtonTypeRoundedRect]; | 
| 160 | self.microphoneButton.frame = CGRectMake(0, 0, 32, 32); | 
| 161 | self.microphoneButton.titleLabel.font = [UIFont fontWithName:kFontAwesomeFont size:20]; | 
| 162 | self.microphoneButton.titleLabel.textAlignment = NSTextAlignmentCenter; | 
| 163 | [self.microphoneButton setTitle:[NSString fa_stringForFontAwesomeIcon:FAMicrophone] | 
| 164 | forState:UIControlStateNormal]; | 
| 165 |  | 
| 166 | self.audioPlaybackController = [[OTRAudioPlaybackController alloc] init]; | 
| 167 |  | 
| 168 | ////// TextViewUpdates ////// | 
| 169 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(receivedTextViewChangedNotification:) name:UITextViewTextDidChangeNotification object:self.inputToolbar.contentView.textView]; | 
| 170 |  | 
| 171 | /** Setup databse view handler*/ | 
| 172 | self.viewHandler = [[OTRYapViewHandler alloc] initWithDatabaseConnection:[OTRDatabaseManager sharedInstance].longLivedReadOnlyConnection databaseChangeNotificationName:[DatabaseNotificationName LongLivedTransactionChanges]]; | 
| 173 | self.viewHandler.delegate = self; | 
| 174 |  | 
| 175 | ///Custom Layout to account for no bubble cells | 
| 176 | OTRMessagesCollectionViewFlowLayout *layout = [[OTRMessagesCollectionViewFlowLayout alloc] init]; | 
| 177 | layout.sizeDelegate = self; | 
| 178 | self.collectionView.collectionViewLayout = layout; | 
| 179 |  | 
| 180 | ///"Loading Earlier" header view | 
| 181 | [self.collectionView registerNib:[UINib nibWithNibName:@"OTRMessagesLoadingView" bundle:OTRAssets.resourcesBundle] | 
| 182 | forSupplementaryViewOfKind:UICollectionElementKindSectionHeader | 
| 183 | withReuseIdentifier:[JSQMessagesLoadEarlierHeaderView headerReuseIdentifier]]; | 
| 184 |  | 
| 185 | //Subscribe to changes in encryption state | 
| 186 | __weak typeof(self)weakSelf = self; | 
| 187 |     [self.KVOController observe:self.state keyPath:NSStringFromSelector(@selector(messageSecurity)) options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
 | 
| 188 | __typeof__(self) strongSelf = weakSelf; | 
| 189 |         if (!strongSelf) { return; }
 | 
| 190 |  | 
| 191 |         if ([object isKindOfClass:[MessagesViewControllerState class]]) {
 | 
| 192 | MessagesViewControllerState *state = (MessagesViewControllerState*)object; | 
| 193 | NSString * placeHolderString = nil; | 
| 194 |             switch (state.messageSecurity) {
 | 
| 195 | case OTRMessageTransportSecurityPlaintext: | 
| 196 | case OTRMessageTransportSecurityPlaintextWithOTR: | 
| 197 | placeHolderString = SEND_PLAINTEXT_STRING(); | 
| 198 | break; | 
| 199 | case OTRMessageTransportSecurityOTR: | 
| 200 | placeHolderString = [NSString stringWithFormat:SEND_ENCRYPTED_STRING(),@"OTR"]; | 
| 201 | break; | 
| 202 | case OTRMessageTransportSecurityOMEMO: | 
| 203 | placeHolderString = [NSString stringWithFormat:SEND_ENCRYPTED_STRING(),@"OMEMO"];; | 
| 204 | break; | 
| 205 |  | 
| 206 | default: | 
| 207 | placeHolderString = [NSBundle jsq_localizedStringForKey:@"new_message"]; | 
| 208 | break; | 
| 209 | } | 
| 210 | strongSelf.inputToolbar.contentView.textView.placeHolder = placeHolderString; | 
| 211 | [self didUpdateState]; | 
| 212 | } | 
| 213 | }]; | 
| 214 |  | 
| 215 | } | 
| 216 |  | 
| 217 | - (void)viewDidAppear:(BOOL)animated | 
| 218 | {
 | 
| 219 | [super viewDidAppear:animated]; | 
| 220 | [self tryToMarkAllMessagesAsRead]; | 
| 221 | // This is a hack to attempt fixing https://github.com/ChatSecure/ChatSecure-iOS/issues/657 | 
| 222 |     dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
 | 
| 223 | [self scrollToBottomAnimated:animated]; | 
| 224 | }); | 
| 225 | self.loadingMessages = NO; | 
| 226 | } | 
| 227 |  | 
| 228 | - (void)viewWillAppear:(BOOL)animated | 
| 229 | {
 | 
| 230 | self.currentIndexPath = nil; | 
| 231 |  | 
| 232 | [super viewWillAppear:animated]; | 
| 233 | [[UIApplication sharedApplication] setStatusBarHidden:NO]; | 
| 234 |  | 
| 235 |     if (self.lastSeenRefreshTimer) {
 | 
| 236 | [self.lastSeenRefreshTimer invalidate]; | 
| 237 | _lastSeenRefreshTimer = nil; | 
| 238 | } | 
| 239 | _lastSeenRefreshTimer = [NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(refreshTitleTimerUpdate:) userInfo:nil repeats:YES]; | 
| 240 |  | 
| 241 | __weak typeof(self)weakSelf = self; | 
| 242 |     void (^refreshGeneratingLock)(OTRAccount *) = ^void(OTRAccount * account) {
 | 
| 243 | __strong typeof(weakSelf)strongSelf = weakSelf; | 
| 244 | __block NSString *accountKey = nil; | 
| 245 |         [strongSelf.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
 | 
| 246 | accountKey = [strongSelf buddyWithTransaction:transaction].accountUniqueId; | 
| 247 | }]; | 
| 248 |         if ([account.uniqueId isEqualToString:accountKey]) {
 | 
| 249 | [strongSelf updateEncryptionState]; | 
| 250 | } | 
| 251 |  | 
| 252 |  | 
| 253 | }; | 
| 254 |  | 
| 255 |     self.didFinishGeneratingPrivateKeyNotificationObject = [[NSNotificationCenter defaultCenter] addObserverForName:OTRDidFinishGeneratingPrivateKeyNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
 | 
| 256 |         if ([note.object isKindOfClass:[OTRAccount class]]) {
 | 
| 257 | refreshGeneratingLock(note.object); | 
| 258 | } | 
| 259 | }]; | 
| 260 |  | 
| 261 |     self.messageStateDidChangeNotificationObject = [[NSNotificationCenter defaultCenter] addObserverForName:OTRMessageStateDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
 | 
| 262 | __strong typeof(weakSelf)strongSelf = weakSelf; | 
| 263 |         if ([note.object isKindOfClass:[OTRBuddy class]]) {
 | 
| 264 | OTRBuddy *notificationBuddy = note.object; | 
| 265 | __block NSString *buddyKey = nil; | 
| 266 |             [strongSelf.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
 | 
| 267 | buddyKey = [strongSelf buddyWithTransaction:transaction].uniqueId; | 
| 268 | }]; | 
| 269 |             if ([notificationBuddy.uniqueId isEqualToString:buddyKey]) {
 | 
| 270 | [strongSelf updateEncryptionState]; | 
| 271 | } | 
| 272 | } | 
| 273 | }]; | 
| 274 |  | 
| 275 |     if ([self.threadKey length]) {
 | 
| 276 | [self.viewHandler.keyCollectionObserver observe:self.threadKey collection:self.threadCollection]; | 
| 277 | [self updateViewWithKey:self.threadKey collection:self.threadCollection]; | 
| 278 | [self.viewHandler setup:OTRFilteredChatDatabaseViewExtensionName groups:@[self.threadKey]]; | 
| 279 |         if(![self.inputToolbar.contentView.textView.text length]) {
 | 
| 280 | [self moveLastComposingTextForThreadKey:self.threadKey colleciton:self.threadCollection toTextView:self.inputToolbar.contentView.textView]; | 
| 281 | } | 
| 282 | } | 
| 283 |  | 
| 284 | self.loadingMessages = YES; | 
| 285 | [self.collectionView reloadData]; | 
| 286 | } | 
| 287 |  | 
| 288 | - (void)viewWillDisappear:(BOOL)animated | 
| 289 | {
 | 
| 290 | [super viewWillDisappear:animated]; | 
| 291 |  | 
| 292 | [self.lastSeenRefreshTimer invalidate]; | 
| 293 | self.lastSeenRefreshTimer = nil; | 
| 294 |  | 
| 295 | [self saveCurrentMessageText:self.inputToolbar.contentView.textView.text threadKey:self.threadKey colleciton:self.threadCollection]; | 
| 296 |  | 
| 297 | [[NSNotificationCenter defaultCenter] removeObserver:self.messageStateDidChangeNotificationObject]; | 
| 298 | [[NSNotificationCenter defaultCenter] removeObserver:self.didFinishGeneratingPrivateKeyNotificationObject]; | 
| 299 |  | 
| 300 | // [self.inputToolbar.contentView.textView resignFirstResponder]; | 
| 301 | } | 
| 302 |  | 
| 303 | - (void)viewDidDisappear:(BOOL)animated | 
| 304 | {
 | 
| 305 | [super viewDidDisappear:animated]; | 
| 306 |  | 
| 307 | _warningAvatarImage = nil; | 
| 308 | _accountAvatarImage = nil; | 
| 309 | _buddyAvatarImage = nil; | 
| 310 | } | 
| 311 |  | 
| 312 | - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
 | 
| 313 | [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; | 
| 314 |  | 
| 315 | // After the transition is done, we need to reset the size caches and relayout | 
| 316 | // Do this using the technique in https://stackoverflow.com/questions/26943808/ios-how-to-run-a-function-after-device-has-rotated-swift | 
| 317 |     [coordinator animateAlongsideTransition:nil completion:^(id<UIViewControllerTransitionCoordinatorContext>  _Nonnull context) {
 | 
| 318 | [self.messageSizeCache removeAllObjects]; | 
| 319 | [self.collectionView.collectionViewLayout invalidateLayoutWithContext:[JSQMessagesCollectionViewFlowLayoutInvalidationContext context]]; | 
| 320 | }]; | 
| 321 | } | 
| 322 |  | 
| 323 | #pragma - mark Setters & getters | 
| 324 |  | 
| 325 | - (OTRAttachmentPicker *)attachmentPicker | 
| 326 | {
 | 
| 327 |     if (!_attachmentPicker) {
 | 
| 328 | _attachmentPicker = [[OTRAttachmentPicker alloc] initWithParentViewController:self delegate:self]; | 
| 329 | } | 
| 330 | return _attachmentPicker; | 
| 331 | } | 
| 332 |  | 
| 333 | - (NSArray*) indexPathsToCount:(NSUInteger)count {
 | 
| 334 | NSMutableArray *indexPaths = [NSMutableArray arrayWithCapacity:count]; | 
| 335 |     for (NSUInteger i = 0; i < count; i++) {
 | 
| 336 | NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0]; | 
| 337 | [indexPaths addObject:indexPath]; | 
| 338 | } | 
| 339 | return indexPaths; | 
| 340 | } | 
| 341 |  | 
| 342 | - (nullable id<OTRThreadOwner>)threadObjectWithTransaction:(nonnull YapDatabaseReadTransaction *)transaction {
 | 
| 343 |     if (!self.threadKey || !self.threadCollection || !transaction) { return nil; }
 | 
| 344 | id object = [transaction objectForKey:self.threadKey inCollection:self.threadCollection]; | 
| 345 |     if ([object conformsToProtocol:@protocol(OTRThreadOwner)]) {
 | 
| 346 | return object; | 
| 347 | } | 
| 348 | return nil; | 
| 349 | } | 
| 350 |  | 
| 351 | - (nullable OTRBuddy *)buddyWithTransaction:(nonnull YapDatabaseReadTransaction *)transaction {
 | 
| 352 | id <OTRThreadOwner> object = [self threadObjectWithTransaction:transaction]; | 
| 353 |     if ([object isKindOfClass:[OTRBuddy class]]) {
 | 
| 354 | return (OTRBuddy *)object; | 
| 355 | } | 
| 356 | return nil; | 
| 357 | } | 
| 358 |  | 
| 359 | - (nullable OTRXMPPRoom *)roomWithTransaction:(nonnull YapDatabaseReadTransaction *)transaction {
 | 
| 360 | id <OTRThreadOwner> object = [self threadObjectWithTransaction:transaction]; | 
| 361 |     if ([object isKindOfClass:[OTRXMPPRoom class]]) {
 | 
| 362 | return (OTRXMPPRoom *)object; | 
| 363 | } | 
| 364 | return nil; | 
| 365 | } | 
| 366 |  | 
| 367 | - (nullable OTRAccount *)accountWithTransaction:(nonnull YapDatabaseReadTransaction *)transaction {
 | 
| 368 | id <OTRThreadOwner> thread = [self threadObjectWithTransaction:transaction]; | 
| 369 |     if (!thread) { return nil; }
 | 
| 370 | OTRAccount *account = [OTRAccount fetchObjectWithUniqueID:[thread threadAccountIdentifier] transaction:transaction]; | 
| 371 | return account; | 
| 372 | } | 
| 373 |  | 
| 374 | - (void)setThreadKey:(NSString *)key collection:(NSString *)collection | 
| 375 | {
 | 
| 376 | self.currentIndexPath = nil; | 
| 377 | NSString *oldKey = self.threadKey; | 
| 378 | NSString *oldCollection = self.threadCollection; | 
| 379 |  | 
| 380 | self.threadKey = key; | 
| 381 | self.threadCollection = collection; | 
| 382 |     [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
 | 
| 383 | self.senderId = [[self threadObjectWithTransaction:transaction] threadAccountIdentifier]; | 
| 384 | }]; | 
| 385 |  | 
| 386 | // Clear out old state (don't just alloc a new object, we have KVOs attached to this!) | 
| 387 | self.state.canSendMedia = NO; | 
| 388 | self.state.canKnock = NO; | 
| 389 | self.state.messageSecurity = OTRMessageTransportSecurityInvalid; | 
| 390 | self.state.hasText = NO; | 
| 391 | self.state.isThreadOnline = NO; | 
| 392 | self.showTypingIndicator = NO; | 
| 393 |  | 
| 394 | // This is set to nil so the refreshTitleView: method knows to reset username instead of last seen time | 
| 395 | [self titleView].subtitleLabel.text = nil; | 
| 396 |  | 
| 397 |     if (![oldKey isEqualToString:key] || ![oldCollection isEqualToString:collection]) {
 | 
| 398 | [self saveCurrentMessageText:self.inputToolbar.contentView.textView.text threadKey:oldKey colleciton:oldCollection]; | 
| 399 | self.inputToolbar.contentView.textView.text = nil; | 
| 400 | [self receivedTextViewChanged:self.inputToolbar.contentView.textView]; | 
| 401 | } | 
| 402 |  | 
| 403 | [self.viewHandler.keyCollectionObserver stopObserving:oldKey collection:oldCollection]; | 
| 404 |     if (self.threadKey && self.threadCollection) {
 | 
| 405 | [self.viewHandler.keyCollectionObserver observe:self.threadKey collection:self.threadCollection]; | 
| 406 | [self updateViewWithKey:self.threadKey collection:self.threadCollection]; | 
| 407 | [self.viewHandler setup:OTRFilteredChatDatabaseViewExtensionName groups:@[self.threadKey]]; | 
| 408 | [self moveLastComposingTextForThreadKey:self.threadKey colleciton:self.threadCollection toTextView:self.inputToolbar.contentView.textView]; | 
| 409 |     } else {
 | 
| 410 | // Reset the view handler | 
| 411 | self.viewHandler = [[OTRYapViewHandler alloc] initWithDatabaseConnection:[OTRDatabaseManager sharedInstance].longLivedReadOnlyConnection databaseChangeNotificationName:[DatabaseNotificationName LongLivedTransactionChanges]]; | 
| 412 | self.viewHandler.delegate = self; | 
| 413 | self.senderDisplayName = @""; | 
| 414 | self.senderId = @""; | 
| 415 | } | 
| 416 |  | 
| 417 | [self.collectionView reloadData]; | 
| 418 |  | 
| 419 | // Profile Info Button | 
| 420 | [self setupInfoButton]; | 
| 421 |  | 
| 422 | [self updateEncryptionState]; | 
| 423 | [self updateJIDForwardingHeader]; | 
| 424 |  | 
| 425 | __weak typeof(self)weakSelf = self; | 
| 426 |     if (self.pendingApprovalDidChangeNotificationObject == nil) {
 | 
| 427 |         self.pendingApprovalDidChangeNotificationObject = [[NSNotificationCenter defaultCenter] addObserverForName:OTRBuddyPendingApprovalDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
 | 
| 428 | __strong typeof(weakSelf)strongSelf = weakSelf; | 
| 429 | OTRXMPPBuddy *notificationBuddy = [note.userInfo objectForKey:@"buddy"]; | 
| 430 | __block NSString *buddyKey = nil; | 
| 431 |             [strongSelf.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
 | 
| 432 | buddyKey = [strongSelf buddyWithTransaction:transaction].uniqueId; | 
| 433 | }]; | 
| 434 |             if ([notificationBuddy.uniqueId isEqualToString:buddyKey]) {
 | 
| 435 | [strongSelf fetchOMEMODeviceList]; | 
| 436 | [strongSelf sendPresenceProbe]; | 
| 437 | } | 
| 438 | }]; | 
| 439 | } | 
| 440 |  | 
| 441 |     if (self.deviceListUpdateNotificationObject == nil) {
 | 
| 442 |         self.deviceListUpdateNotificationObject = [[NSNotificationCenter defaultCenter] addObserverForName:OTROMEMOSignalCoordinator.DeviceListUpdateNotificationName object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
 | 
| 443 | __strong typeof(weakSelf)strongSelf = weakSelf; | 
| 444 | XMPPJID *notificationJid = [note.userInfo objectForKey:@"jid"]; | 
| 445 | __block NSString *buddyUser = nil; | 
| 446 |             [strongSelf.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
 | 
| 447 | buddyUser = [strongSelf buddyWithTransaction:transaction].username; | 
| 448 | }]; | 
| 449 |             if (notificationJid != nil && [notificationJid.bare isEqualToString:buddyUser]) {
 | 
| 450 | [strongSelf updateEncryptionState]; | 
| 451 | } | 
| 452 | }]; | 
| 453 | } | 
| 454 |  | 
| 455 | [self sendPresenceProbe]; | 
| 456 | [self fetchOMEMODeviceList]; | 
| 457 | } | 
| 458 |  | 
| 459 |  | 
| 460 | - (YapDatabaseConnection *)readOnlyDatabaseConnection | 
| 461 | {
 | 
| 462 |     if (!_readOnlyDatabaseConnection) {
 | 
| 463 | _readOnlyDatabaseConnection = [OTRDatabaseManager sharedInstance].readOnlyDatabaseConnection; | 
| 464 | } | 
| 465 | return _readOnlyDatabaseConnection; | 
| 466 | } | 
| 467 |  | 
| 468 | - (YapDatabaseConnection *)readWriteDatabaseConnection | 
| 469 | {
 | 
| 470 |     if (!_readWriteDatabaseConnection) {
 | 
| 471 | _readWriteDatabaseConnection = [OTRDatabaseManager sharedInstance].readWriteDatabaseConnection; | 
| 472 | } | 
| 473 | return _readWriteDatabaseConnection; | 
| 474 | } | 
| 475 |  | 
| 476 |  | 
| 477 | - (nullable OTRXMPPManager *)xmppManagerWithTransaction:(nonnull YapDatabaseReadTransaction *)transaction {
 | 
| 478 | OTRAccount *account = [self accountWithTransaction:transaction]; | 
| 479 |     if (!account) { return nil; }
 | 
| 480 | return (OTRXMPPManager *)[[OTRProtocolManager sharedInstance] protocolForAccount:account]; | 
| 481 | } | 
| 482 |  | 
| 483 | /** Will send a probe to fetch last seen */ | 
| 484 | - (void) sendPresenceProbe {
 | 
| 485 | __block OTRXMPPManager *xmpp = nil; | 
| 486 | __block OTRXMPPBuddy *buddy = nil; | 
| 487 |     [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
 | 
| 488 | xmpp = [self xmppManagerWithTransaction:transaction]; | 
| 489 | buddy = (OTRXMPPBuddy*)[self buddyWithTransaction:transaction]; | 
| 490 | }]; | 
| 491 |     if (!xmpp || ![buddy isKindOfClass:[OTRXMPPBuddy class]] || buddy.pendingApproval) { return; }
 | 
| 492 | [xmpp sendPresenceProbeForBuddy:buddy]; | 
| 493 | } | 
| 494 |  | 
| 495 | - (void)updateViewWithKey:(NSString *)key collection:(NSString *)collection | 
| 496 | {
 | 
| 497 |     if ([collection isEqualToString:[OTRBuddy collection]]) {
 | 
| 498 | __block OTRBuddy *buddy = nil; | 
| 499 | __block OTRAccount *account = nil; | 
| 500 |         [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
 | 
| 501 | buddy = [OTRBuddy fetchObjectWithUniqueID:key transaction:transaction]; | 
| 502 | account = [OTRAccount fetchObjectWithUniqueID:buddy.accountUniqueId transaction:transaction]; | 
| 503 | }]; | 
| 504 |  | 
| 505 |  | 
| 506 |  | 
| 507 | //Update UI now | 
| 508 |         if (buddy.chatState == OTRChatStateComposing || buddy.chatState == OTRChatStatePaused) {
 | 
| 509 | self.showTypingIndicator = YES; | 
| 510 | } | 
| 511 |         else {
 | 
| 512 | self.showTypingIndicator = NO; | 
| 513 | } | 
| 514 |  | 
| 515 | // Update Buddy Status | 
| 516 | BOOL previousState = self.state.isThreadOnline; | 
| 517 | self.state.isThreadOnline = buddy.status != OTRThreadStatusOffline; | 
| 518 |  | 
| 519 | [self didUpdateState]; | 
| 520 |  | 
| 521 | //Update Buddy knock status | 
| 522 | //Async because this calls down to the database and iterates over a relation. Might slowdown the UI if on main thread | 
| 523 | __weak __typeof__(self) weakSelf = self; | 
| 524 |         dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
 | 
| 525 | __typeof__(self) strongSelf = weakSelf; | 
| 526 | __block BOOL canKnock = [[[OTRProtocolManager sharedInstance].pushController pushStorage] numberOfTokensForBuddy:buddy.uniqueId createdByThisAccount:NO] > 0; | 
| 527 |             dispatch_async(dispatch_get_main_queue(), ^{
 | 
| 528 |                 if (canKnock != strongSelf.state.canKnock) {
 | 
| 529 | strongSelf.state.canKnock = canKnock; | 
| 530 | [strongSelf didUpdateState]; | 
| 531 | } | 
| 532 | }); | 
| 533 |  | 
| 534 | }); | 
| 535 |  | 
| 536 | [self refreshTitleView:[self titleView]]; | 
| 537 |  | 
| 538 | // Auto-inititate OTR when contact comes online | 
| 539 |         if (!previousState && self.state.isThreadOnline) {
 | 
| 540 | [[OTRProtocolManager sharedInstance].encryptionManager maybeRefreshOTRSessionForBuddyKey:key collection:collection]; | 
| 541 | } | 
| 542 |     } else if ([collection isEqualToString:[OTRXMPPRoom collection]]) {
 | 
| 543 | __block OTRXMPPRoom *room = nil; | 
| 544 |         [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
 | 
| 545 | room = [OTRXMPPRoom fetchObjectWithUniqueID:key transaction:transaction]; | 
| 546 | }]; | 
| 547 | self.state.isThreadOnline = room.currentStatus != OTRThreadStatusOffline; | 
| 548 | [self didUpdateState]; | 
| 549 | [self refreshTitleView:[self titleView]]; | 
| 550 | } | 
| 551 | [self tryToMarkAllMessagesAsRead]; | 
| 552 | } | 
| 553 |  | 
| 554 | - (void)tryToMarkAllMessagesAsRead {
 | 
| 555 | // Set all messages as read | 
| 556 |     if ([self otr_isVisible]) {
 | 
| 557 | __weak __typeof__(self) weakSelf = self; | 
| 558 | __block id <OTRThreadOwner>threadOwner = nil; | 
| 559 | __block NSArray <id <OTRMessageProtocol>>* unreadMessages = nil; | 
| 560 |         [self.readOnlyDatabaseConnection asyncReadWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
 | 
| 561 | threadOwner = [weakSelf threadObjectWithTransaction:transaction]; | 
| 562 |             if (!threadOwner) { return; }
 | 
| 563 | unreadMessages = [transaction allUnreadMessagesForThread:threadOwner]; | 
| 564 |         } completionBlock:^{
 | 
| 565 |  | 
| 566 |             if ([unreadMessages count] == 0) {
 | 
| 567 | return; | 
| 568 | } | 
| 569 |  | 
| 570 | //Mark as read | 
| 571 |  | 
| 572 | NSMutableArray <id <OTRMessageProtocol>>*toBeSaved = [[NSMutableArray alloc] init]; | 
| 573 |  | 
| 574 |             [unreadMessages enumerateObjectsUsingBlock:^(id<OTRMessageProtocol>  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
 | 
| 575 |                 if ([obj isKindOfClass:[OTRIncomingMessage class]]) {
 | 
| 576 | OTRIncomingMessage *message = [((OTRIncomingMessage *)obj) copy]; | 
| 577 | message.read = YES; | 
| 578 | [toBeSaved addObject:message]; | 
| 579 |                 } else if ([obj isKindOfClass:[OTRXMPPRoomMessage class]]) {
 | 
| 580 | OTRXMPPRoomMessage *message = [((OTRXMPPRoomMessage *)obj) copy]; | 
| 581 | message.read = YES; | 
| 582 | [toBeSaved addObject:message]; | 
| 583 | } | 
| 584 | }]; | 
| 585 |  | 
| 586 |             [weakSelf.readWriteDatabaseConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) {
 | 
| 587 |                 [toBeSaved enumerateObjectsUsingBlock:^(id<OTRMessageProtocol>  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
 | 
| 588 | [transaction setObject:obj forKey:[obj messageKey] inCollection:[obj messageCollection]]; | 
| 589 | }]; | 
| 590 | [transaction touchObjectForKey:[threadOwner threadIdentifier] inCollection:[threadOwner threadCollection]]; | 
| 591 | }]; | 
| 592 | }]; | 
| 593 | } | 
| 594 | } | 
| 595 |  | 
| 596 | - (OTRTitleSubtitleView * __nonnull)titleView {
 | 
| 597 | UIView *titleView = self.navigationItem.titleView; | 
| 598 |     if ([titleView isKindOfClass:[OTRTitleSubtitleView class]]) {
 | 
| 599 | return (OTRTitleSubtitleView*)titleView; | 
| 600 | } | 
| 601 | return [[OTRTitleSubtitleView alloc] initWithFrame:CGRectMake(0, 0, 200, 44)]; | 
| 602 | } | 
| 603 |  | 
| 604 | - (void)refreshTitleTimerUpdate:(NSTimer*)timer {
 | 
| 605 | [self refreshTitleView:[self titleView]]; | 
| 606 | } | 
| 607 |  | 
| 608 | /** Updates the title view with the current thread information on this view controller*/ | 
| 609 | - (void)refreshTitleView:(OTRTitleSubtitleView *)titleView | 
| 610 | {
 | 
| 611 | __block id<OTRThreadOwner> thread = nil; | 
| 612 | __block OTRAccount *account = nil; | 
| 613 |     [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
 | 
| 614 | thread = [self threadObjectWithTransaction:transaction]; | 
| 615 | account = [self accountWithTransaction:transaction]; | 
| 616 | }]; | 
| 617 |  | 
| 618 | titleView.titleLabel.text = [thread threadName]; | 
| 619 |  | 
| 620 | UIImage *statusImage = nil; | 
| 621 |     if ([thread isKindOfClass:[OTRBuddy class]]) {
 | 
| 622 | OTRBuddy *buddy = (OTRBuddy*)thread; | 
| 623 | UIColor *color = [buddy avatarBorderColor]; | 
| 624 |         if (color) { // only show online status
 | 
| 625 | statusImage = [OTRImages circleWithRadius:50 | 
| 626 | lineWidth:0 | 
| 627 | lineColor:nil | 
| 628 | fillColor:color]; | 
| 629 | } | 
| 630 |  | 
| 631 |         dispatch_block_t refreshTimeBlock = ^{
 | 
| 632 | __block OTRBuddy *buddy = nil; | 
| 633 |             [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
 | 
| 634 | buddy = (OTRBuddy*)[self threadObjectWithTransaction:transaction]; | 
| 635 | }]; | 
| 636 |             if (![buddy isKindOfClass:[OTRBuddy class]]) {
 | 
| 637 | return; | 
| 638 | } | 
| 639 | NSDate *lastSeen = [OTRBuddyCache.shared lastSeenDateForBuddy:buddy]; | 
| 640 | OTRThreadStatus status = [OTRBuddyCache.shared threadStatusForBuddy:buddy]; | 
| 641 |             if (!lastSeen) {
 | 
| 642 | titleView.subtitleLabel.text = buddy.username; | 
| 643 | return; | 
| 644 | } | 
| 645 | TTTTimeIntervalFormatter *tf = [[TTTTimeIntervalFormatter alloc] init]; | 
| 646 | tf.presentTimeIntervalMargin = 60; | 
| 647 | tf.usesAbbreviatedCalendarUnits = YES; | 
| 648 | NSTimeInterval lastSeenInterval = [lastSeen timeIntervalSinceDate:[NSDate date]]; | 
| 649 | NSString *labelString = nil; | 
| 650 |             if (status == OTRThreadStatusAvailable) {
 | 
| 651 | labelString = buddy.username; | 
| 652 |             } else {
 | 
| 653 | labelString = [NSString stringWithFormat:@"%@ %@", ACTIVE_STRING(), [tf stringForTimeInterval:lastSeenInterval]]; | 
| 654 | } | 
| 655 | titleView.subtitleLabel.text = labelString; | 
| 656 | }; | 
| 657 |  | 
| 658 | // Set the username if nothing else is set. | 
| 659 | // This should be cleared out when buddy is changed | 
| 660 |         if (!titleView.subtitleLabel.text) {
 | 
| 661 | titleView.subtitleLabel.text = buddy.username; | 
| 662 | } | 
| 663 |  | 
| 664 | // Show an "Last seen 11 min ago" in title bar after brief delay | 
| 665 |         dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
 | 
| 666 | refreshTimeBlock(); | 
| 667 | }); | 
| 668 |     } else if ([thread isGroupThread]) {
 | 
| 669 | titleView.subtitleLabel.text = GROUP_CHAT_STRING(); | 
| 670 |     } else {
 | 
| 671 | titleView.subtitleLabel.text = nil; | 
| 672 | } | 
| 673 |  | 
| 674 | titleView.titleImageView.image = statusImage; | 
| 675 |  | 
| 676 | } | 
| 677 |  | 
| 678 | /** | 
| 679 | This generates a UIAlertAction where the handler fetches the outgoing message (optionaly duplicates). Then if media message resend media message. If not update messageSecurityInfo and date and create new sending action. | 
| 680 | */ | 
| 681 | - (UIAlertAction *)resendOutgoingMessageActionForMessageKey:(NSString *)messageKey | 
| 682 | messageCollection:(NSString *)messageCollection | 
| 683 | readWriteDatabaseConnection:(YapDatabaseConnection*)databaseConnection | 
| 684 | title:(NSString *)title | 
| 685 | {
 | 
| 686 |     UIAlertAction *action = [UIAlertAction actionWithTitle:title style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
 | 
| 687 |         [databaseConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) {
 | 
| 688 | id object = [[transaction objectForKey:messageKey inCollection:messageCollection] copy]; | 
| 689 | id<OTRMessageProtocol> message = nil; | 
| 690 |             if ([object conformsToProtocol:@protocol(OTRMessageProtocol)]) {
 | 
| 691 | message = (id<OTRMessageProtocol>)object; | 
| 692 |             } else {
 | 
| 693 | return; | 
| 694 | } | 
| 695 | // Messages that never sent properly don't need to be duplicated client-side | 
| 696 | NSError *messageError = message.messageError; | 
| 697 | message = [message duplicateMessage]; | 
| 698 | message.messageError = nil; | 
| 699 | message.messageSecurity = self.state.messageSecurity; | 
| 700 | message.messageDate = [NSDate date]; | 
| 701 | [message saveWithTransaction:transaction]; | 
| 702 |  | 
| 703 | // We only need to re-upload failed media messages | 
| 704 | // otherwise just resend the URL directly | 
| 705 | if (message.messageMediaItemKey.length && | 
| 706 |                 (!message.messageText.length || messageError)) {
 | 
| 707 | OTRMediaItem *mediaItem = [OTRMediaItem fetchObjectWithUniqueID:message.messageMediaItemKey transaction:transaction]; | 
| 708 | [self sendMediaItem:mediaItem data:nil message:message transaction:transaction]; | 
| 709 |             } else {
 | 
| 710 | OTRYapMessageSendAction *sendingAction = [OTRYapMessageSendAction sendActionForMessage:message date:message.messageDate]; | 
| 711 | [sendingAction saveWithTransaction:transaction]; | 
| 712 | } | 
| 713 | }]; | 
| 714 | }]; | 
| 715 | return action; | 
| 716 | } | 
| 717 |  | 
| 718 | - (nonnull UIAlertAction *)viewProfileAction {
 | 
| 719 |     return [UIAlertAction actionWithTitle:VIEW_PROFILE_STRING() style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
 | 
| 720 | [self infoButtonPressed:action]; | 
| 721 | }]; | 
| 722 | } | 
| 723 |  | 
| 724 | - (nonnull UIAlertAction *)cancleAction {
 | 
| 725 | return [UIAlertAction actionWithTitle:CANCEL_STRING() | 
| 726 | style:UIAlertActionStyleCancel | 
| 727 | handler:nil]; | 
| 728 | } | 
| 729 |  | 
| 730 | - (NSArray <UIAlertAction *>*)actionForMessage:(id<OTRMessageProtocol>)message {
 | 
| 731 | NSMutableArray <UIAlertAction *>*actions = [[NSMutableArray alloc] init]; | 
| 732 |  | 
| 733 |     if (!message.isMessageIncoming) {
 | 
| 734 | // This is an outgoing message so we can offer to resend | 
| 735 | UIAlertAction *resendAction = [self resendOutgoingMessageActionForMessageKey:message.messageKey messageCollection:message.messageCollection readWriteDatabaseConnection:self.readWriteDatabaseConnection title:RESEND_STRING()]; | 
| 736 | [actions addObject:resendAction]; | 
| 737 | } | 
| 738 |  | 
| 739 |     if (![message isKindOfClass:[OTRXMPPRoomMessage class]]) {
 | 
| 740 | [actions addObject:[self viewProfileAction]]; | 
| 741 | } | 
| 742 |  | 
| 743 | NSArray<UIAlertAction*> *mediaActions = [UIAlertAction actionsForMediaMessage:message sourceView:self.view viewController:self]; | 
| 744 | [actions addObjectsFromArray:mediaActions]; | 
| 745 |  | 
| 746 | [actions addObject:[self cancleAction]]; | 
| 747 | return actions; | 
| 748 | } | 
| 749 |  | 
| 750 | - (void)didTapAvatar:(id<OTRMessageProtocol>)message sender:(id)sender {
 | 
| 751 | NSError *error = [message messageError]; | 
| 752 | NSString *title = nil; | 
| 753 | NSString *alertMessage = nil; | 
| 754 |  | 
| 755 | NSString * sendingType = UNENCRYPTED_STRING(); | 
| 756 |     switch (self.state.messageSecurity) {
 | 
| 757 | case OTRMessageTransportSecurityOTR: | 
| 758 | sendingType = @"OTR"; | 
| 759 | break; | 
| 760 | case OTRMessageTransportSecurityOMEMO: | 
| 761 | sendingType = @"OMEMO"; | 
| 762 | break; | 
| 763 |  | 
| 764 | default: | 
| 765 | break; | 
| 766 | } | 
| 767 |  | 
| 768 |     if ([message isKindOfClass:[OTROutgoingMessage class]]) {
 | 
| 769 | title = RESEND_MESSAGE_TITLE(); | 
| 770 | alertMessage = [NSString stringWithFormat:RESEND_DESCRIPTION_STRING(),sendingType]; | 
| 771 | } | 
| 772 |  | 
| 773 |     if (error) {
 | 
| 774 | NSUInteger otrFingerprintError = 32872; | 
| 775 | title = ERROR_STRING(); | 
| 776 | alertMessage = error.localizedDescription; | 
| 777 |  | 
| 778 |         if (error.code == otrFingerprintError) {
 | 
| 779 | alertMessage = NO_DEVICES_BUDDY_ERROR_STRING(); | 
| 780 | } | 
| 781 |  | 
| 782 |         if([message isKindOfClass:[OTROutgoingMessage class]]) {
 | 
| 783 | //If it's an outgoing message the error title should be that we were unable to send the message. | 
| 784 | title = UNABLE_TO_SEND_STRING(); | 
| 785 |  | 
| 786 |  | 
| 787 |  | 
| 788 | NSString *resendDescription = [NSString stringWithFormat:RESEND_DESCRIPTION_STRING(),sendingType]; | 
| 789 | alertMessage = [alertMessage stringByAppendingString:[NSString stringWithFormat:@"\n%@",resendDescription]]; | 
| 790 |  | 
| 791 | //If this is an error about not having a trusted identity then we should offer to connect to the | 
| 792 | if (error.code == OTROMEMOErrorNoDevicesForBuddy || | 
| 793 | error.code == OTROMEMOErrorNoDevices || | 
| 794 |                 error.code == otrFingerprintError) {
 | 
| 795 |  | 
| 796 | alertMessage = [alertMessage stringByAppendingString:[NSString stringWithFormat:@"\n%@",VIEW_PROFILE_DESCRIPTION_STRING()]]; | 
| 797 | } | 
| 798 | } | 
| 799 | } | 
| 800 |  | 
| 801 |  | 
| 802 |     if (![self isMessageTrusted:message]) {
 | 
| 803 | title = UNTRUSTED_DEVICE_STRING(); | 
| 804 |         if ([message isMessageIncoming]) {
 | 
| 805 | alertMessage = UNTRUSTED_DEVICE_REVEIVED_STRING(); | 
| 806 |         } else {
 | 
| 807 | alertMessage = UNTRUSTED_DEVICE_SENT_STRING(); | 
| 808 | } | 
| 809 | alertMessage = [alertMessage stringByAppendingString:[NSString stringWithFormat:@"\n%@",VIEW_PROFILE_DESCRIPTION_STRING()]]; | 
| 810 | } | 
| 811 |  | 
| 812 | NSArray <UIAlertAction*>*actions = [self actionForMessage:message]; | 
| 813 |     if ([actions count] > 0) {
 | 
| 814 | UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title message:alertMessage preferredStyle:UIAlertControllerStyleActionSheet]; | 
| 815 |         [actions enumerateObjectsUsingBlock:^(UIAlertAction * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
 | 
| 816 | [alertController addAction:obj]; | 
| 817 | }]; | 
| 818 |         if ([sender isKindOfClass:[UIView class]]) {
 | 
| 819 | UIView *sourceView = sender; | 
| 820 | alertController.popoverPresentationController.sourceView = sourceView; | 
| 821 | alertController.popoverPresentationController.sourceRect = sourceView.bounds; | 
| 822 | } | 
| 823 | [self presentViewController:alertController animated:YES completion:nil]; | 
| 824 | } | 
| 825 | } | 
| 826 |  | 
| 827 | - (BOOL)isMessageTrusted:(id <OTRMessageProtocol>)message {
 | 
| 828 | BOOL trusted = YES; | 
| 829 |     if (![message isKindOfClass:[OTRBaseMessage class]]) {
 | 
| 830 | return trusted; | 
| 831 | } | 
| 832 |  | 
| 833 | OTRBaseMessage *baseMessage = (OTRBaseMessage *)message; | 
| 834 |  | 
| 835 |  | 
| 836 |     if (baseMessage.messageSecurityInfo.messageSecurity == OTRMessageTransportSecurityOTR) {
 | 
| 837 | NSData *otrFingerprintData = baseMessage.messageSecurityInfo.otrFingerprint; | 
| 838 |         if ([otrFingerprintData length]) {
 | 
| 839 | trusted = [[[OTRProtocolManager sharedInstance].encryptionManager otrFingerprintForKey:self.threadKey collection:self.threadCollection fingerprint:otrFingerprintData] isTrusted]; | 
| 840 | } | 
| 841 |     } else if (baseMessage.messageSecurityInfo.messageSecurity == OTRMessageTransportSecurityOMEMO) {
 | 
| 842 | NSString *omemoDeviceYapKey = baseMessage.messageSecurityInfo.omemoDeviceYapKey; | 
| 843 | NSString *omemoDeviceYapCollection = baseMessage.messageSecurityInfo.omemoDeviceYapCollection; | 
| 844 | __block OTROMEMODevice *device = nil; | 
| 845 |         [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
 | 
| 846 | device = [transaction objectForKey:omemoDeviceYapKey inCollection:omemoDeviceYapCollection]; | 
| 847 | }]; | 
| 848 |         if(device != nil) {
 | 
| 849 | trusted = [device isTrusted]; | 
| 850 | } | 
| 851 | } | 
| 852 | return trusted; | 
| 853 | } | 
| 854 |  | 
| 855 | - (BOOL) isGroupChat {
 | 
| 856 | __block OTRXMPPRoom *room = nil; | 
| 857 |     [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
 | 
| 858 | room = [self roomWithTransaction:transaction]; | 
| 859 | }]; | 
| 860 | return (room != nil); | 
| 861 | } | 
| 862 |  | 
| 863 | #pragma - mark Profile Button Methods | 
| 864 |  | 
| 865 | - (void)setupInfoButton {
 | 
| 866 |     if ([self isGroupChat]) {
 | 
| 867 | UIBarButtonItem *barButtonItem = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"112-group" inBundle:[OTRAssets resourcesBundle] compatibleWithTraitCollection:nil] style:UIBarButtonItemStylePlain target:self action:@selector(didSelectOccupantsButton:)]; | 
| 868 | self.navigationItem.rightBarButtonItem = barButtonItem; | 
| 869 |     } else {
 | 
| 870 | UIButton* infoButton = [UIButton buttonWithType:UIButtonTypeInfoLight]; | 
| 871 | infoButton.accessibilityIdentifier = @"profileButton"; | 
| 872 | [infoButton addTarget:self action:@selector(infoButtonPressed:) forControlEvents:UIControlEventTouchUpInside]; | 
| 873 | self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:infoButton]; | 
| 874 | } | 
| 875 | } | 
| 876 |  | 
| 877 | - (void) infoButtonPressed:(id)sender {
 | 
| 878 | __block OTRAccount *account = nil; | 
| 879 | __block OTRBuddy *buddy = nil; | 
| 880 |     [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
 | 
| 881 | account = [self accountWithTransaction:transaction]; | 
| 882 | buddy = [self buddyWithTransaction:transaction]; | 
| 883 | }]; | 
| 884 |     if (!account || !buddy) {
 | 
| 885 | return; | 
| 886 | } | 
| 887 |  | 
| 888 | // Hack to manually re-fetch OMEMO devicelist because PEP sucks | 
| 889 | // TODO: Ideally this should be moved to some sort of manual refresh in the Profile view | 
| 890 | [self fetchOMEMODeviceList]; | 
| 891 |  | 
| 892 | XLFormDescriptor *form = [UserProfileViewController profileFormDescriptorForAccount:account buddies:@[buddy] connection:self.readOnlyDatabaseConnection]; | 
| 893 |  | 
| 894 | UserProfileViewController *verify = [[UserProfileViewController alloc] initWithAccountKey:account.uniqueId connection:self.readOnlyDatabaseConnection form:form]; | 
| 895 |     verify.completionBlock = ^{
 | 
| 896 | [self updateEncryptionState]; | 
| 897 | }; | 
| 898 | UINavigationController *verifyNav = [[UINavigationController alloc] initWithRootViewController:verify]; | 
| 899 | verifyNav.modalPresentationStyle = UIModalPresentationFormSheet; | 
| 900 | [self presentViewController:verifyNav animated:YES completion:nil]; | 
| 901 | } | 
| 902 |  | 
| 903 | - (void)didSelectOccupantsButton:(id)sender {
 | 
| 904 | UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"OTRRoomOccupants" bundle:[OTRAssets resourcesBundle]]; | 
| 905 | OTRRoomOccupantsViewController *occupantsVC = [storyboard instantiateViewControllerWithIdentifier:@"roomOccupants"]; | 
| 906 | occupantsVC.delegate = self; | 
| 907 | [occupantsVC setupViewHandlerWithDatabaseConnection:[OTRDatabaseManager sharedInstance].longLivedReadOnlyConnection roomKey:self.threadKey]; | 
| 908 | [self.navigationController pushViewController:occupantsVC animated:YES]; | 
| 909 | } | 
| 910 |  | 
| 911 | // Hack to manually re-fetch OMEMO devicelist because PEP sucks | 
| 912 | // TODO: Ideally this should be moved to some sort of manual refresh in the Profile view | 
| 913 | -(void) fetchOMEMODeviceList {
 | 
| 914 | __block OTRAccount *account = nil; | 
| 915 | __block OTRBuddy *buddy = nil; | 
| 916 |     [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
 | 
| 917 | account = [self accountWithTransaction:transaction]; | 
| 918 | buddy = [self buddyWithTransaction:transaction]; | 
| 919 | }]; | 
| 920 |     if (!account || !buddy) {
 | 
| 921 | return; | 
| 922 | } | 
| 923 | id manager = [[OTRProtocolManager sharedInstance] protocolForAccount:account]; | 
| 924 |     if ([manager isKindOfClass:[OTRXMPPManager class]]) {
 | 
| 925 | XMPPJID *jid = [XMPPJID jidWithString:buddy.username]; | 
| 926 | OTRXMPPManager *xmpp = manager; | 
| 927 | [xmpp.omemoSignalCoordinator.omemoModule fetchDeviceIdsForJID:jid elementId:nil]; | 
| 928 | } | 
| 929 | } | 
| 930 |  | 
| 931 | - (UIBarButtonItem *)rightBarButtonItem | 
| 932 | {
 | 
| 933 |     if (!self.lockBarButtonItem) {
 | 
| 934 | self.lockBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:self.lockButton]; | 
| 935 | } | 
| 936 | return self.lockBarButtonItem; | 
| 937 | } | 
| 938 |  | 
| 939 | -(void)updateEncryptionState | 
| 940 | {
 | 
| 941 |     if ([self isGroupChat]) {
 | 
| 942 | __block OTRXMPPManager *xmpp = nil; | 
| 943 |         [self.readOnlyDatabaseConnection asyncReadWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
 | 
| 944 | xmpp = [self xmppManagerWithTransaction:transaction]; | 
| 945 |         } completionBlock:^{
 | 
| 946 | BOOL canSendMedia = NO; | 
| 947 | // Check for XEP-0363 HTTP upload | 
| 948 | // TODO: move this check elsewhere so it isnt dependent on refreshing crypto state | 
| 949 |             if (xmpp != nil && xmpp.fileTransferManager.canUploadFiles) {
 | 
| 950 | canSendMedia = YES; | 
| 951 | } | 
| 952 | self.state.canSendMedia = canSendMedia; | 
| 953 | self.state.messageSecurity = OTRMessageTransportSecurityPlaintext; | 
| 954 | [self didUpdateState]; | 
| 955 | }]; | 
| 956 |     } else {
 | 
| 957 | __block OTRBuddy *buddy = nil; | 
| 958 | __block OTRAccount *account = nil; | 
| 959 | __block OTRXMPPManager *xmpp = nil; | 
| 960 | __block OTRMessageTransportSecurity messageSecurity = OTRMessageTransportSecurityInvalid; | 
| 961 |  | 
| 962 |         [self.readOnlyDatabaseConnection asyncReadWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
 | 
| 963 | buddy = [self buddyWithTransaction:transaction]; | 
| 964 | account = [buddy accountWithTransaction:transaction]; | 
| 965 | xmpp = [self xmppManagerWithTransaction:transaction]; | 
| 966 | messageSecurity = [buddy preferredTransportSecurityWithTransaction:transaction]; | 
| 967 |         } completionBlock:^{
 | 
| 968 | BOOL canSendMedia = NO; | 
| 969 | // Check for XEP-0363 HTTP upload | 
| 970 | // TODO: move this check elsewhere so it isnt dependent on refreshing crypto state | 
| 971 |             if (xmpp != nil && xmpp.fileTransferManager.canUploadFiles) {
 | 
| 972 | canSendMedia = YES; | 
| 973 | } | 
| 974 |             if (!buddy || !account || !xmpp || (messageSecurity == OTRMessageTransportSecurityInvalid)) {
 | 
| 975 | DDLogError(@"updateEncryptionState error: missing parameters"); | 
| 976 |             } else {
 | 
| 977 | OTRKitMessageState messageState = [[OTRProtocolManager sharedInstance].encryptionManager.otrKit messageStateForUsername:buddy.username accountName:account.username protocol:account.protocolTypeString]; | 
| 978 | if (messageState == OTRKitMessageStateEncrypted && | 
| 979 |                     buddy.status != OTRThreadStatusOffline) {
 | 
| 980 | // If other side supports OTR, assume OTRDATA is possible | 
| 981 | canSendMedia = YES; | 
| 982 | } | 
| 983 | } | 
| 984 | self.state.canSendMedia = canSendMedia; | 
| 985 | self.state.messageSecurity = messageSecurity; | 
| 986 | [self didUpdateState]; | 
| 987 | }]; | 
| 988 | } | 
| 989 | } | 
| 990 |  | 
| 991 | - (void)setupAccessoryButtonsWithMessageState:(OTRKitMessageState)messageState buddyStatus:(OTRThreadStatus)status textViewHasText:(BOOL)hasText | 
| 992 | {
 | 
| 993 | self.inputToolbar.contentView.rightBarButtonItem = self.sendButton; | 
| 994 | self.inputToolbar.sendButtonLocation = JSQMessagesInputSendButtonLocationRight; | 
| 995 | self.inputToolbar.contentView.leftBarButtonItem = nil; | 
| 996 | } | 
| 997 |  | 
| 998 | - (void)connectButtonPressed:(id)sender | 
| 999 | {
 | 
| 1000 | [self hideDropdownAnimated:YES completion:nil]; | 
| 1001 | __block OTRAccount *account = nil; | 
| 1002 |     [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
 | 
| 1003 | account = [self accountWithTransaction:transaction]; | 
| 1004 | }]; | 
| 1005 |  | 
| 1006 |     if (account == nil) {
 | 
| 1007 | return; | 
| 1008 | } | 
| 1009 |  | 
| 1010 | //If we have the password then we can login with that password otherwise show login UI to enter password | 
| 1011 |     if ([account.password length]) {
 | 
| 1012 | [[OTRProtocolManager sharedInstance] loginAccount:account userInitiated:YES]; | 
| 1013 |  | 
| 1014 |     } else {
 | 
| 1015 | OTRBaseLoginViewController *loginViewController = [[OTRBaseLoginViewController alloc] initWithAccount:account]; | 
| 1016 | UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:loginViewController]; | 
| 1017 | nav.modalPresentationStyle = UIModalPresentationFormSheet; | 
| 1018 | [self presentViewController:nav animated:YES completion:nil]; | 
| 1019 | } | 
| 1020 |  | 
| 1021 |  | 
| 1022 | } | 
| 1023 |  | 
| 1024 | #pragma - mark dropDown Methods | 
| 1025 |  | 
| 1026 | - (void)showDropdownWithTitle:(NSString *)title buttons:(NSArray *)buttons animated:(BOOL)animated tag:(NSInteger)tag | 
| 1027 | {
 | 
| 1028 | NSTimeInterval duration = 0.3; | 
| 1029 |     if (!animated) {
 | 
| 1030 | duration = 0.0; | 
| 1031 | } | 
| 1032 |  | 
| 1033 | self.buttonDropdownView = [[OTRButtonView alloc] initWithTitle:title buttons:buttons]; | 
| 1034 | self.buttonDropdownView.tag = tag; | 
| 1035 |  | 
| 1036 | CGFloat height = [OTRButtonView heightForTitle:title width:self.view.bounds.size.width buttons:buttons]; | 
| 1037 |  | 
| 1038 | [self.view addSubview:self.buttonDropdownView]; | 
| 1039 |  | 
| 1040 | [self.buttonDropdownView autoSetDimension:ALDimensionHeight toSize:height]; | 
| 1041 | [self.buttonDropdownView autoPinEdgeToSuperviewEdge:ALEdgeLeading]; | 
| 1042 | [self.buttonDropdownView autoPinEdgeToSuperviewEdge:ALEdgeTrailing]; | 
| 1043 | self.buttonDropdownView.topLayoutConstraint = [self.buttonDropdownView autoPinToTopLayoutGuideOfViewController:self withInset:height*-1]; | 
| 1044 |  | 
| 1045 | [self.buttonDropdownView layoutIfNeeded]; | 
| 1046 |  | 
| 1047 |     [UIView animateWithDuration:duration animations:^{
 | 
| 1048 | self.buttonDropdownView.topLayoutConstraint.constant = 0.0; | 
| 1049 | [self.buttonDropdownView layoutIfNeeded]; | 
| 1050 | } completion:nil]; | 
| 1051 |  | 
| 1052 | } | 
| 1053 |  | 
| 1054 | - (void)hideDropdownAnimated:(BOOL)animated completion:(void (^)(void))completion | 
| 1055 | {
 | 
| 1056 |     if (!self.buttonDropdownView) {
 | 
| 1057 |         if (completion) {
 | 
| 1058 | completion(); | 
| 1059 | } | 
| 1060 | } | 
| 1061 |     else {
 | 
| 1062 | NSTimeInterval duration = 0.3; | 
| 1063 |         if (!animated) {
 | 
| 1064 | duration = 0.0; | 
| 1065 | } | 
| 1066 |  | 
| 1067 |         [UIView animateWithDuration:duration animations:^{
 | 
| 1068 | CGFloat height = self.buttonDropdownView.frame.size.height; | 
| 1069 | self.buttonDropdownView.topLayoutConstraint.constant = height*-1; | 
| 1070 | [self.buttonDropdownView layoutIfNeeded]; | 
| 1071 |  | 
| 1072 |         } completion:^(BOOL finished) {
 | 
| 1073 |             if (finished) {
 | 
| 1074 | [self.buttonDropdownView removeFromSuperview]; | 
| 1075 | self.buttonDropdownView = nil; | 
| 1076 | } | 
| 1077 |  | 
| 1078 |             if (completion) {
 | 
| 1079 | completion(); | 
| 1080 | } | 
| 1081 | }]; | 
| 1082 | } | 
| 1083 | } | 
| 1084 |  | 
| 1085 | - (void)saveCurrentMessageText:(NSString *)text threadKey:(NSString *)key colleciton:(NSString *)collection | 
| 1086 | {
 | 
| 1087 |     if (![key length] || ![collection length]) {
 | 
| 1088 | return; | 
| 1089 | } | 
| 1090 |  | 
| 1091 |     [self.readWriteDatabaseConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) {
 | 
| 1092 | id <OTRThreadOwner> thread = [[transaction objectForKey:key inCollection:collection] copy]; | 
| 1093 |         if (thread == nil) {
 | 
| 1094 | // this can happen when we've just approved a contact, then the thread key | 
| 1095 | // might have changed. | 
| 1096 | return; | 
| 1097 | } | 
| 1098 | [thread setCurrentMessageText:text]; | 
| 1099 | [transaction setObject:thread forKey:key inCollection:collection]; | 
| 1100 |  | 
| 1101 | //Send inactive chat State | 
| 1102 | OTRAccount *account = [OTRAccount fetchObjectWithUniqueID:[thread threadAccountIdentifier] transaction:transaction]; | 
| 1103 | OTRXMPPManager *xmppManager = (OTRXMPPManager *)[[OTRProtocolManager sharedInstance] protocolForAccount:account]; | 
| 1104 |         if (![text length]) {
 | 
| 1105 | [xmppManager sendChatState:OTRChatStateInactive withBuddyID:[thread threadIdentifier]]; | 
| 1106 | } | 
| 1107 | }]; | 
| 1108 | } | 
| 1109 |  | 
| 1110 | //* Takes the current value out of the thread object and sets it to the text view and nils out result*/ | 
| 1111 | - (void)moveLastComposingTextForThreadKey:(NSString *)key colleciton:(NSString *)collection toTextView:(UITextView *)textView {
 | 
| 1112 |     if (![key length] || ![collection length] || !textView) {
 | 
| 1113 | return; | 
| 1114 | } | 
| 1115 | __block id <OTRThreadOwner> thread = nil; | 
| 1116 |     [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
 | 
| 1117 | thread = [[transaction objectForKey:key inCollection:collection] copy]; | 
| 1118 | }]; | 
| 1119 | // Don't remove text you're already composing | 
| 1120 | NSString *oldThreadText = [thread currentMessageText]; | 
| 1121 |     if (!textView.text.length && oldThreadText.length) {
 | 
| 1122 | textView.text = oldThreadText; | 
| 1123 | [self receivedTextViewChanged:textView]; | 
| 1124 | } | 
| 1125 |     if (oldThreadText.length) {
 | 
| 1126 | [thread setCurrentMessageText:nil]; | 
| 1127 |         [self.readWriteDatabaseConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) {
 | 
| 1128 | [transaction setObject:thread forKey:key inCollection:collection]; | 
| 1129 | }]; | 
| 1130 | } | 
| 1131 | } | 
| 1132 |  | 
| 1133 | - (id <OTRMessageProtocol,JSQMessageData>)messageAtIndexPath:(NSIndexPath *)indexPath | 
| 1134 | {
 | 
| 1135 | // Multiple invocations with the same indexPath tend to come in groups, no need to hit the DB each time. | 
| 1136 | // Even though the object is cached, the row ID calculation still takes time | 
| 1137 |     if (![indexPath isEqual:self.currentIndexPath]) {
 | 
| 1138 | self.currentIndexPath = indexPath; | 
| 1139 | self.currentMessage = [self.viewHandler object:indexPath]; | 
| 1140 | } | 
| 1141 | return self.currentMessage; | 
| 1142 | } | 
| 1143 |  | 
| 1144 | /** | 
| 1145 | * Updates the flexible range of the DB connection. | 
| 1146 | * @param reset When NO, adds kOTRMessagePageSize to the range length, when YES resets the length to the kOTRMessagePageSize | 
| 1147 | */ | 
| 1148 | - (void)updateRangeOptions:(BOOL)reset | 
| 1149 | {
 | 
| 1150 | YapDatabaseViewRangeOptions *options = [self.viewHandler.mappings rangeOptionsForGroup:self.threadKey]; | 
| 1151 |     if (reset) {
 | 
| 1152 |         if (options != nil && !self.messageRangeExtended) {
 | 
| 1153 | return; | 
| 1154 | } | 
| 1155 | options = [YapDatabaseViewRangeOptions flexibleRangeWithLength:kOTRMessagePageSize | 
| 1156 | offset:0 | 
| 1157 | from:YapDatabaseViewEnd]; | 
| 1158 | self.messageSizeCache.countLimit = kOTRMessagePageSize; | 
| 1159 | self.messageRangeExtended = NO; | 
| 1160 |     } else {
 | 
| 1161 | options = [options copyWithNewLength:options.length + kOTRMessagePageSize]; | 
| 1162 | self.messageSizeCache.countLimit += kOTRMessagePageSize; | 
| 1163 | self.messageRangeExtended = YES; | 
| 1164 | } | 
| 1165 | [self.viewHandler.mappings setRangeOptions:options forGroup:self.threadKey]; | 
| 1166 |  | 
| 1167 | self.loadingMessages = YES; | 
| 1168 |  | 
| 1169 | CGFloat distanceToBottom = self.collectionView.contentSize.height - self.collectionView.contentOffset.y; | 
| 1170 |  | 
| 1171 | [self.collectionView reloadData]; | 
| 1172 |  | 
| 1173 |     [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
 | 
| 1174 | NSUInteger shownCount = [self.viewHandler.mappings numberOfItemsInGroup:self.threadKey]; | 
| 1175 | NSUInteger totalCount = [[transaction ext:OTRFilteredChatDatabaseViewExtensionName] numberOfItemsInGroup:self.threadKey]; | 
| 1176 | [self setShowLoadEarlierMessagesHeader:shownCount < totalCount]; | 
| 1177 | }]; | 
| 1178 |  | 
| 1179 |     if (!reset) {
 | 
| 1180 | [self.collectionView.collectionViewLayout invalidateLayout]; | 
| 1181 | [self.collectionView layoutSubviews]; | 
| 1182 | self.collectionView.contentOffset = CGPointMake(0, self.collectionView.contentSize.height - distanceToBottom); | 
| 1183 | } | 
| 1184 |  | 
| 1185 | self.loadingMessages = NO; | 
| 1186 | } | 
| 1187 |  | 
| 1188 | - (BOOL)showDateAtIndexPath:(NSIndexPath *)indexPath | 
| 1189 | {
 | 
| 1190 | BOOL showDate = NO; | 
| 1191 |     if (indexPath.row == 0) {
 | 
| 1192 | showDate = YES; | 
| 1193 | } | 
| 1194 |     else {
 | 
| 1195 | id <OTRMessageProtocol> currentMessage = [self messageAtIndexPath:indexPath]; | 
| 1196 | id <OTRMessageProtocol> previousMessage = [self messageAtIndexPath:[NSIndexPath indexPathForItem:indexPath.row-1 inSection:indexPath.section]]; | 
| 1197 |  | 
| 1198 | NSTimeInterval timeDifference = [[currentMessage messageDate] timeIntervalSinceDate:[previousMessage messageDate]]; | 
| 1199 |         if (timeDifference > kOTRMessageSentDateShowTimeInterval) {
 | 
| 1200 | showDate = YES; | 
| 1201 | } | 
| 1202 | } | 
| 1203 | return showDate; | 
| 1204 | } | 
| 1205 |  | 
| 1206 | - (BOOL)showSenderDisplayNameAtIndexPath:(NSIndexPath *)indexPath {
 | 
| 1207 | id<OTRMessageProtocol,JSQMessageData> message = [self messageAtIndexPath:indexPath]; | 
| 1208 |  | 
| 1209 |     if(![self.threadCollection isEqualToString:[OTRXMPPRoom collection]]) {
 | 
| 1210 | return NO; | 
| 1211 | } | 
| 1212 |  | 
| 1213 |     if ([[message senderId] isEqualToString:self.senderId]) {
 | 
| 1214 | return NO; | 
| 1215 | } | 
| 1216 |  | 
| 1217 |     if(indexPath.row -1 >= 0) {
 | 
| 1218 | NSIndexPath *previousIndexPath = [NSIndexPath indexPathForRow:indexPath.row - 1 inSection:indexPath.section]; | 
| 1219 | id<OTRMessageProtocol,JSQMessageData> previousMessage = [self messageAtIndexPath:previousIndexPath]; | 
| 1220 |         if ([[previousMessage senderId] isEqualToString:message.senderId]) {
 | 
| 1221 | return NO; | 
| 1222 | } | 
| 1223 | } | 
| 1224 |  | 
| 1225 | return YES; | 
| 1226 | } | 
| 1227 |  | 
| 1228 | - (BOOL)isPushMessageAtIndexPath:(NSIndexPath *)indexPath {
 | 
| 1229 | id message = [self messageAtIndexPath:indexPath]; | 
| 1230 | return [message isKindOfClass:[PushMessage class]]; | 
| 1231 | } | 
| 1232 |  | 
| 1233 | - (void)receivedTextViewChangedNotification:(NSNotification *)notification | 
| 1234 | {
 | 
| 1235 | //Check if the text state changes from having some text to some or vice versa | 
| 1236 | UITextView *textView = notification.object; | 
| 1237 | [self receivedTextViewChanged:textView]; | 
| 1238 | } | 
| 1239 |  | 
| 1240 | - (void)receivedTextViewChanged:(UITextView *)textView {
 | 
| 1241 | BOOL hasText = [textView.text length] > 0; | 
| 1242 |     if(hasText != self.state.hasText) {
 | 
| 1243 | self.state.hasText = hasText; | 
| 1244 | [self didUpdateState]; | 
| 1245 | } | 
| 1246 |  | 
| 1247 | //Everytime the textview has text and a notification comes through we are 'typing' otherwise we are done typing | 
| 1248 |     if (hasText) {
 | 
| 1249 | [self isTyping]; | 
| 1250 |     } else {
 | 
| 1251 | [self didFinishTyping]; | 
| 1252 | } | 
| 1253 |  | 
| 1254 | return; | 
| 1255 |  | 
| 1256 | } | 
| 1257 |  | 
| 1258 | #pragma - mark Update UI | 
| 1259 |  | 
| 1260 | - (void)didUpdateState {
 | 
| 1261 |  | 
| 1262 | } | 
| 1263 |  | 
| 1264 | - (void)isTyping {
 | 
| 1265 |  | 
| 1266 | } | 
| 1267 |  | 
| 1268 | - (void)didFinishTyping {
 | 
| 1269 |  | 
| 1270 | } | 
| 1271 |  | 
| 1272 | #pragma - mark Sending Media Items | 
| 1273 |  | 
| 1274 | - (void)sendMediaItem:(OTRMediaItem *)mediaItem data:(NSData *)data message:(id<OTRMessageProtocol>)message transaction:(YapDatabaseReadWriteTransaction *)transaction | 
| 1275 | {
 | 
| 1276 | id<OTRThreadOwner> thread = [self threadObjectWithTransaction:transaction]; | 
| 1277 | OTRXMPPManager *xmpp = [self xmppManagerWithTransaction:transaction]; | 
| 1278 |     if (!message || !thread || !xmpp) {
 | 
| 1279 | DDLogError(@"Error sending file due to bad paramters"); | 
| 1280 | return; | 
| 1281 | } | 
| 1282 |     if (data) {
 | 
| 1283 | thread.lastMessageIdentifier = message.messageKey; | 
| 1284 | [thread saveWithTransaction:transaction]; | 
| 1285 | } | 
| 1286 | // XEP-0363 | 
| 1287 | [xmpp.fileTransferManager sendWithMediaItem:mediaItem prefetchedData:data message:message]; | 
| 1288 |  | 
| 1289 | [mediaItem touchParentMessageWithTransaction:transaction]; | 
| 1290 | } | 
| 1291 |  | 
| 1292 | #pragma - mark Media Display Methods | 
| 1293 |  | 
| 1294 | - (void)showImage:(OTRImageItem *)imageItem fromCollectionView:(JSQMessagesCollectionView *)collectionView atIndexPath:(NSIndexPath *)indexPath | 
| 1295 | {
 | 
| 1296 | //FIXME: Possible for image to not be in cache? | 
| 1297 | UIImage *image = [OTRImages imageWithIdentifier:imageItem.uniqueId]; | 
| 1298 | JTSImageInfo *imageInfo = [[JTSImageInfo alloc] init]; | 
| 1299 | imageInfo.image = image; | 
| 1300 |  | 
| 1301 | UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath]; | 
| 1302 |     if ([cell isKindOfClass:[JSQMessagesCollectionViewCell class]]) {
 | 
| 1303 | UIView *cellContainterView = ((JSQMessagesCollectionViewCell *)cell).messageBubbleContainerView; | 
| 1304 | imageInfo.referenceRect = cellContainterView.bounds; | 
| 1305 | imageInfo.referenceView = cellContainterView; | 
| 1306 | imageInfo.referenceCornerRadius = 10; | 
| 1307 | } | 
| 1308 |  | 
| 1309 | JTSImageViewController *imageViewer = [[JTSImageViewController alloc] | 
| 1310 | initWithImageInfo:imageInfo | 
| 1311 | mode:JTSImageViewControllerMode_Image | 
| 1312 | backgroundStyle:JTSImageViewControllerBackgroundOption_Blurred]; | 
| 1313 |  | 
| 1314 | [imageViewer showFromViewController:self transition:JTSImageViewControllerTransition_FromOriginalPosition]; | 
| 1315 | } | 
| 1316 |  | 
| 1317 | - (void)showVideo:(OTRVideoItem *)videoItem fromCollectionView:(JSQMessagesCollectionView *)collectionView atIndexPath:(NSIndexPath *)indexPath | 
| 1318 | {
 | 
| 1319 |     if (videoItem.filename) {
 | 
| 1320 | NSURL *videoURL = [[OTRMediaServer sharedInstance] urlForMediaItem:videoItem buddyUniqueId:self.threadKey]; | 
| 1321 | MPMoviePlayerViewController *moviePlayerViewController = [[MPMoviePlayerViewController alloc] initWithContentURL:videoURL]; | 
| 1322 | [self presentViewController:moviePlayerViewController animated:YES completion:nil]; | 
| 1323 | } | 
| 1324 | } | 
| 1325 |  | 
| 1326 | - (void)playOrPauseAudio:(OTRAudioItem *)audioItem fromCollectionView:(JSQMessagesCollectionView *)collectionView atIndexPath:(NSIndexPath *)indexPath | 
| 1327 | {
 | 
| 1328 | NSError *error = nil; | 
| 1329 |     if  ([audioItem.uniqueId isEqualToString:self.audioPlaybackController.currentAudioItem.uniqueId]) {
 | 
| 1330 |         if  ([self.audioPlaybackController isPlaying]) {
 | 
| 1331 | [self.audioPlaybackController pauseCurrentlyPlaying]; | 
| 1332 | } | 
| 1333 |         else {
 | 
| 1334 | [self.audioPlaybackController resumeCurrentlyPlaying]; | 
| 1335 | } | 
| 1336 | } | 
| 1337 |     else {
 | 
| 1338 | [self.audioPlaybackController stopCurrentlyPlaying]; | 
| 1339 | OTRAudioControlsView *audioControls = [self audioControllsfromCollectionView:collectionView atIndexPath:indexPath]; | 
| 1340 | [self.audioPlaybackController attachAudioControlsView:audioControls]; | 
| 1341 | [self.audioPlaybackController playAudioItem:audioItem buddyUniqueId:self.threadKey error:&error]; | 
| 1342 | } | 
| 1343 |  | 
| 1344 |     if (error) {
 | 
| 1345 | DDLogError(@"Audio Playback Error: %@",error); | 
| 1346 | } | 
| 1347 |  | 
| 1348 | } | 
| 1349 |  | 
| 1350 | - (OTRAudioControlsView *)audioControllsfromCollectionView:(JSQMessagesCollectionView *)collectionView atIndexPath:(NSIndexPath *)indexPath {
 | 
| 1351 | UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath]; | 
| 1352 |     if ([cell isKindOfClass:[JSQMessagesCollectionViewCell class]]) {
 | 
| 1353 | UIView *mediaView = ((JSQMessagesCollectionViewCell *)cell).mediaView; | 
| 1354 | UIView *view = [mediaView viewWithTag:kOTRAudioControlsViewTag]; | 
| 1355 |         if ([view isKindOfClass:[OTRAudioControlsView class]]) {
 | 
| 1356 | return (OTRAudioControlsView *)view; | 
| 1357 | } | 
| 1358 | } | 
| 1359 |  | 
| 1360 | return nil; | 
| 1361 | } | 
| 1362 |  | 
| 1363 | #pragma MARK - OTRMessagesCollectionViewFlowLayoutSizeProtocol methods | 
| 1364 |  | 
| 1365 | - (BOOL)hasBubbleSizeForCellAtIndexPath:(NSIndexPath *)indexPath {
 | 
| 1366 | return ![self isPushMessageAtIndexPath:indexPath]; | 
| 1367 | } | 
| 1368 |  | 
| 1369 | #pragma mark - JSQMessagesViewController method overrides | 
| 1370 |  | 
| 1371 | - (UICollectionViewCell *)collectionView:(JSQMessagesCollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath | 
| 1372 | {
 | 
| 1373 | JSQMessagesCollectionViewCell *cell = (JSQMessagesCollectionViewCell *)[super collectionView:collectionView cellForItemAtIndexPath:indexPath]; | 
| 1374 |  | 
| 1375 | //Fixes times when there needs to be two lines (date & knock sent) and doesn't seem to affect one line instances | 
| 1376 | cell.cellTopLabel.numberOfLines = 0; | 
| 1377 |  | 
| 1378 | id <OTRMessageProtocol>message = [self messageAtIndexPath:indexPath]; | 
| 1379 |  | 
| 1380 | __block OTRXMPPAccount *account = nil; | 
| 1381 |     [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
 | 
| 1382 | account = (OTRXMPPAccount*)[self accountWithTransaction:transaction]; | 
| 1383 | }]; | 
| 1384 |  | 
| 1385 | UIColor *textColor = nil; | 
| 1386 |     if ([message isMessageIncoming]) {
 | 
| 1387 | textColor = [UIColor blackColor]; | 
| 1388 | } | 
| 1389 |     else {
 | 
| 1390 | textColor = [UIColor whiteColor]; | 
| 1391 | } | 
| 1392 | if (cell.textView != nil) | 
| 1393 | cell.textView.textColor = textColor; | 
| 1394 |  | 
| 1395 | // Do not allow clickable links for Tor accounts to prevent information leakage | 
| 1396 | // Could be better to move this information to the message object to not need to do a database read. | 
| 1397 |     if ([account isKindOfClass:[OTRXMPPTorAccount class]]) {
 | 
| 1398 | cell.textView.dataDetectorTypes = UIDataDetectorTypeNone; | 
| 1399 | } | 
| 1400 |     else {
 | 
| 1401 | cell.textView.dataDetectorTypes = UIDataDetectorTypeLink; | 
| 1402 |         cell.textView.linkTextAttributes = @{ NSForegroundColorAttributeName : textColor,
 | 
| 1403 | NSUnderlineStyleAttributeName : @(NSUnderlineStyleSingle | NSUnderlinePatternSolid) }; | 
| 1404 | } | 
| 1405 |  | 
| 1406 |     if ([[message messageMediaItemKey] isEqualToString:self.audioPlaybackController.currentAudioItem.uniqueId]) {
 | 
| 1407 | UIView *view = [cell.mediaView viewWithTag:kOTRAudioControlsViewTag]; | 
| 1408 |         if ([view isKindOfClass:[OTRAudioControlsView class]]) {
 | 
| 1409 | [self.audioPlaybackController attachAudioControlsView:(OTRAudioControlsView *)view]; | 
| 1410 | } | 
| 1411 | } | 
| 1412 |  | 
| 1413 | // Needed for link interaction | 
| 1414 | cell.textView.delegate = self; | 
| 1415 | return cell; | 
| 1416 | } | 
| 1417 |  | 
| 1418 | - (BOOL)collectionView:(UICollectionView *)collectionView canPerformAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender | 
| 1419 | {
 | 
| 1420 |     if (action == @selector(delete:)) {
 | 
| 1421 | return YES; | 
| 1422 | } | 
| 1423 |  | 
| 1424 | return [super collectionView:collectionView canPerformAction:action forItemAtIndexPath:indexPath withSender:sender]; | 
| 1425 | } | 
| 1426 |  | 
| 1427 | - (void)didPressSendButton:(UIButton *)button withMessageText:(NSString *)text senderId:(NSString *)senderId senderDisplayName:(NSString *)senderDisplayName date:(NSDate *)date | 
| 1428 | {
 | 
| 1429 |     if(!text.length) {
 | 
| 1430 | return; | 
| 1431 | } | 
| 1432 |  | 
| 1433 | self.navigationController.providesPresentationContextTransitionStyle = YES; | 
| 1434 | self.navigationController.definesPresentationContext = YES; | 
| 1435 |  | 
| 1436 | //0. Clear out message text immediately | 
| 1437 | // This is to prevent the scenario where multiple messages get sent because the message text isn't cleared out | 
| 1438 | // due to aggregated touch events during UI pauses. | 
| 1439 | // A side effect is that sent messages may not appear in the UI immediately | 
| 1440 | [self finishSendingMessage]; | 
| 1441 |  | 
| 1442 | __block id<OTRMessageProtocol> message = nil; | 
| 1443 | __block OTRXMPPManager *xmpp = nil; | 
| 1444 |     [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
 | 
| 1445 | id<OTRThreadOwner> thread = [self threadObjectWithTransaction:transaction]; | 
| 1446 | message = [thread outgoingMessageWithText:text transaction:transaction]; | 
| 1447 | xmpp = [self xmppManagerWithTransaction:transaction]; | 
| 1448 | }]; | 
| 1449 |     if (!message || !xmpp) { return; }
 | 
| 1450 | [xmpp enqueueMessage:message]; | 
| 1451 | } | 
| 1452 |  | 
| 1453 | - (void)didPressAccessoryButton:(UIButton *)sender | 
| 1454 | {
 | 
| 1455 |     if ([sender isEqual:self.cameraButton]) {
 | 
| 1456 | [self.attachmentPicker showAlertControllerFromSourceView:sender withCompletion:nil]; | 
| 1457 | } | 
| 1458 | } | 
| 1459 |  | 
| 1460 | - (void)collectionView:(UICollectionView *)collectionView performAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender | 
| 1461 | {
 | 
| 1462 |     if (action == @selector(delete:)) {
 | 
| 1463 | [self deleteMessageAtIndexPath:indexPath]; | 
| 1464 | } | 
| 1465 |     else {
 | 
| 1466 | [super collectionView:collectionView performAction:action forItemAtIndexPath:indexPath withSender:sender]; | 
| 1467 | } | 
| 1468 | } | 
| 1469 |  | 
| 1470 | - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath | 
| 1471 | {
 | 
| 1472 | id <OTRMessageProtocol, JSQMessageData> message = [self messageAtIndexPath:indexPath]; | 
| 1473 |  | 
| 1474 | NSNumber *key = @(message.messageHash); | 
| 1475 | NSValue *sizeValue = [self.messageSizeCache objectForKey:key]; | 
| 1476 |     if (sizeValue != nil) {
 | 
| 1477 | return [sizeValue CGSizeValue]; | 
| 1478 | } | 
| 1479 |  | 
| 1480 | // Although JSQMessagesBubblesSizeCalculator has its own cache, its size is fixed and quite small, so it quickly chokes on scrolling into the past | 
| 1481 | CGSize size = [super collectionView:collectionView layout:collectionViewLayout sizeForItemAtIndexPath:indexPath]; | 
| 1482 | // The height of the first cell might change: on loading additional messages the date label most likely will disappear | 
| 1483 |     if (indexPath.row > 0) {
 | 
| 1484 | [self.messageSizeCache setObject:[NSValue valueWithCGSize:size] forKey:key]; | 
| 1485 | } | 
| 1486 | return size; | 
| 1487 | } | 
| 1488 |  | 
| 1489 | #pragma - mark UIPopoverPresentationControllerDelegate Methods | 
| 1490 |  | 
| 1491 | - (void)prepareForPopoverPresentation:(UIPopoverPresentationController *)popoverPresentationController {
 | 
| 1492 | // Without setting this, there will be a crash on iPad | 
| 1493 | // This delegate is set in the OTRAttachmentPicker | 
| 1494 | popoverPresentationController.sourceView = self.cameraButton; | 
| 1495 | } | 
| 1496 |  | 
| 1497 | - (void)sendPhoto:(UIImage *)photo asJPEG:(BOOL)asJPEG shouldResize:(BOOL)shouldResize {
 | 
| 1498 | NSParameterAssert(photo); | 
| 1499 |     if (!photo) { return; }
 | 
| 1500 | __block OTRXMPPManager *xmpp = nil; | 
| 1501 | __block id<OTRThreadOwner> thread = nil; | 
| 1502 |     [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
 | 
| 1503 | xmpp = [self xmppManagerWithTransaction:transaction]; | 
| 1504 | thread = [self threadObjectWithTransaction:transaction]; | 
| 1505 | }]; | 
| 1506 | NSParameterAssert(xmpp); | 
| 1507 | NSParameterAssert(thread); | 
| 1508 |     if (!xmpp || !thread) { return; }
 | 
| 1509 |  | 
| 1510 | [xmpp.fileTransferManager sendWithImage:photo thread:thread]; | 
| 1511 | } | 
| 1512 |  | 
| 1513 | #pragma - mark OTRAttachmentPickerDelegate Methods | 
| 1514 |  | 
| 1515 | - (void)attachmentPicker:(OTRAttachmentPicker *)attachmentPicker gotPhoto:(UIImage *)photo withInfo:(NSDictionary *)info | 
| 1516 | {
 | 
| 1517 | [self sendPhoto:photo asJPEG:YES shouldResize:YES]; | 
| 1518 | } | 
| 1519 |  | 
| 1520 | - (void)attachmentPicker:(OTRAttachmentPicker *)attachmentPicker gotVideoURL:(NSURL *)videoURL | 
| 1521 | {
 | 
| 1522 |     if (!videoURL) { return; }
 | 
| 1523 | __block OTRXMPPManager *xmpp = nil; | 
| 1524 | __block id<OTRThreadOwner> thread = nil; | 
| 1525 |     [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
 | 
| 1526 | xmpp = [self xmppManagerWithTransaction:transaction]; | 
| 1527 | thread = [self threadObjectWithTransaction:transaction]; | 
| 1528 | }]; | 
| 1529 | NSParameterAssert(xmpp); | 
| 1530 | NSParameterAssert(thread); | 
| 1531 |     if (!xmpp || !thread) { return; }
 | 
| 1532 |  | 
| 1533 | [xmpp.fileTransferManager sendWithVideoURL:videoURL thread:thread]; | 
| 1534 | } | 
| 1535 |  | 
| 1536 | - (NSArray <NSString *>*)attachmentPicker:(OTRAttachmentPicker *)attachmentPicker preferredMediaTypesForSource:(UIImagePickerControllerSourceType)source | 
| 1537 | {
 | 
| 1538 | return @[(NSString*)kUTTypeImage]; | 
| 1539 | } | 
| 1540 |  | 
| 1541 | - (void)sendAudioFileURL:(NSURL *)url | 
| 1542 | {
 | 
| 1543 |     if (!url) { return; }
 | 
| 1544 | __block OTRXMPPManager *xmpp = nil; | 
| 1545 | __block id<OTRThreadOwner> thread = nil; | 
| 1546 |     [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
 | 
| 1547 | xmpp = [self xmppManagerWithTransaction:transaction]; | 
| 1548 | thread = [self threadObjectWithTransaction:transaction]; | 
| 1549 | }]; | 
| 1550 | NSParameterAssert(xmpp); | 
| 1551 | NSParameterAssert(thread); | 
| 1552 |     if (!xmpp || !thread) { return; }
 | 
| 1553 |  | 
| 1554 | [xmpp.fileTransferManager sendWithAudioURL:url thread:thread]; | 
| 1555 | } | 
| 1556 |  | 
| 1557 | - (void)sendImageFilePath:(NSString *)filePath asJPEG:(BOOL)asJPEG shouldResize:(BOOL)shouldResize | 
| 1558 | {
 | 
| 1559 | [self sendPhoto:[UIImage imageWithContentsOfFile:filePath] asJPEG:asJPEG shouldResize:shouldResize]; | 
| 1560 | } | 
| 1561 |  | 
| 1562 |  | 
| 1563 | #pragma - mark UIScrollViewDelegate Methods | 
| 1564 |  | 
| 1565 | - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView | 
| 1566 | {
 | 
| 1567 | [self hideDropdownAnimated:YES completion:nil]; | 
| 1568 | } | 
| 1569 |  | 
| 1570 | - (void)scrollViewDidScroll:(UIScrollView *)scrollView | 
| 1571 | {
 | 
| 1572 |     if (!self.loadingMessages) {
 | 
| 1573 | UIEdgeInsets insets = scrollView.contentInset; | 
| 1574 | CGFloat highestOffset = -insets.top; | 
| 1575 | CGFloat lowestOffset = scrollView.contentSize.height - scrollView.frame.size.height + insets.bottom; | 
| 1576 | CGFloat pos = scrollView.contentOffset.y; | 
| 1577 |  | 
| 1578 |         if (self.showLoadEarlierMessagesHeader && (pos == highestOffset || (pos < 0 && (scrollView.isDecelerating || scrollView.isDragging)))) {
 | 
| 1579 | [self updateRangeOptions:NO]; | 
| 1580 |         } else if (pos == lowestOffset) {
 | 
| 1581 | [self updateRangeOptions:YES]; | 
| 1582 | } | 
| 1583 | } | 
| 1584 | } | 
| 1585 |  | 
| 1586 | #pragma mark - UICollectionView DataSource | 
| 1587 | - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section | 
| 1588 | {
 | 
| 1589 | NSInteger numberOfMessages = [self.viewHandler.mappings numberOfItemsInSection:section]; | 
| 1590 | return numberOfMessages; | 
| 1591 | } | 
| 1592 |  | 
| 1593 | #pragma - mark JSQMessagesCollectionViewDataSource Methods | 
| 1594 |  | 
| 1595 | - (NSString *)senderDisplayName | 
| 1596 | {
 | 
| 1597 | __block OTRAccount *account = nil; | 
| 1598 |     [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
 | 
| 1599 | account = [self accountWithTransaction:transaction]; | 
| 1600 | }]; | 
| 1601 |  | 
| 1602 | NSString *senderDisplayName = @""; | 
| 1603 |     if (account) {
 | 
| 1604 |         if ([account.displayName length]) {
 | 
| 1605 | senderDisplayName = account.displayName; | 
| 1606 |         } else {
 | 
| 1607 | senderDisplayName = account.username; | 
| 1608 | } | 
| 1609 | } | 
| 1610 |  | 
| 1611 | return senderDisplayName; | 
| 1612 | } | 
| 1613 |  | 
| 1614 | - (id<JSQMessageData>)collectionView:(JSQMessagesCollectionView *)collectionView messageDataForItemAtIndexPath:(NSIndexPath *)indexPath | 
| 1615 | {
 | 
| 1616 | return (id <JSQMessageData>)[self messageAtIndexPath:indexPath]; | 
| 1617 | } | 
| 1618 |  | 
| 1619 | - (id<JSQMessageBubbleImageDataSource>)collectionView:(JSQMessagesCollectionView *)collectionView messageBubbleImageDataForItemAtIndexPath:(NSIndexPath *)indexPath | 
| 1620 | {
 | 
| 1621 | id <OTRMessageProtocol> message = [self messageAtIndexPath:indexPath]; | 
| 1622 | JSQMessagesBubbleImage *image = nil; | 
| 1623 |     if ([message isMessageIncoming]) {
 | 
| 1624 | image = self.incomingBubbleImage; | 
| 1625 | } | 
| 1626 |     else {
 | 
| 1627 | image = self.outgoingBubbleImage; | 
| 1628 | } | 
| 1629 | return image; | 
| 1630 | } | 
| 1631 |  | 
| 1632 | - (id <JSQMessageAvatarImageDataSource>)collectionView:(JSQMessagesCollectionView *)collectionView avatarImageDataForItemAtIndexPath:(NSIndexPath *)indexPath | 
| 1633 | {
 | 
| 1634 | id <OTRMessageProtocol> message = [self messageAtIndexPath:indexPath]; | 
| 1635 |     if ([message isKindOfClass:[PushMessage class]]) {
 | 
| 1636 | return nil; | 
| 1637 | } | 
| 1638 |  | 
| 1639 | NSError *messageError = [message messageError]; | 
| 1640 | if ((messageError && !messageError.isAutomaticDownloadError) || | 
| 1641 |         ![self isMessageTrusted:message]) {
 | 
| 1642 | return [self warningAvatarImage]; | 
| 1643 | } | 
| 1644 |  | 
| 1645 |     if ([message isKindOfClass:[OTRXMPPRoomMessage class]]) {
 | 
| 1646 | OTRXMPPRoomMessage *roomMessage = (OTRXMPPRoomMessage *)message; | 
| 1647 | __block OTRXMPPRoomOccupant *roomOccupant = nil; | 
| 1648 | __block OTRXMPPBuddy *roomOccupantBuddy = nil; | 
| 1649 |         [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
 | 
| 1650 | roomOccupant = [OTRXMPPRoomOccupant occupantWithJid:[XMPPJID jidWithString:roomMessage.senderJID] realJID:[XMPPJID jidWithString:roomMessage.senderJID] roomJID:[XMPPJID jidWithString:roomMessage.roomJID] accountId:[self accountWithTransaction:transaction].uniqueId createIfNeeded:NO transaction:transaction]; | 
| 1651 |             if (roomOccupant != nil) {
 | 
| 1652 | roomOccupantBuddy = [roomOccupant buddyWith:transaction]; | 
| 1653 | } | 
| 1654 | }]; | 
| 1655 | UIImage *avatarImage = nil; | 
| 1656 |         if (roomOccupant) {
 | 
| 1657 |             if (roomOccupantBuddy != nil) {
 | 
| 1658 | avatarImage = [roomOccupantBuddy avatarImage]; | 
| 1659 | } | 
| 1660 |             if (!avatarImage) {
 | 
| 1661 | avatarImage = [roomOccupant avatarImage]; | 
| 1662 | } | 
| 1663 |         } else {
 | 
| 1664 | avatarImage = [OTRImages avatarImageWithUsername:[[XMPPJID jidWithString:roomMessage.senderJID] resource]]; | 
| 1665 | } | 
| 1666 |         if (avatarImage) {
 | 
| 1667 | NSUInteger diameter = MIN(avatarImage.size.width, avatarImage.size.height); | 
| 1668 | return [JSQMessagesAvatarImageFactory avatarImageWithImage:avatarImage diameter:diameter]; | 
| 1669 | } | 
| 1670 | } | 
| 1671 |  | 
| 1672 |     if ([message isMessageIncoming]) {
 | 
| 1673 | return [self buddyAvatarImage]; | 
| 1674 | } | 
| 1675 |  | 
| 1676 | return [self accountAvatarImage]; | 
| 1677 | } | 
| 1678 |  | 
| 1679 | - (JSQMessagesAvatarImage *)createAvatarImage:(UIImage *(^)(YapDatabaseReadTransaction *))getImage | 
| 1680 | {
 | 
| 1681 | __block UIImage *avatarImage; | 
| 1682 |     [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
 | 
| 1683 | avatarImage = getImage(transaction); | 
| 1684 | }]; | 
| 1685 |     if (avatarImage != nil) {
 | 
| 1686 | NSUInteger diameter = (NSUInteger) MIN(avatarImage.size.width, avatarImage.size.height); | 
| 1687 | return [JSQMessagesAvatarImageFactory avatarImageWithImage:avatarImage diameter:diameter]; | 
| 1688 | } | 
| 1689 | return nil; | 
| 1690 | } | 
| 1691 |  | 
| 1692 | - (JSQMessagesAvatarImage *)warningAvatarImage | 
| 1693 | {
 | 
| 1694 |     if (_warningAvatarImage == nil) {
 | 
| 1695 |         _warningAvatarImage = [self createAvatarImage:^(YapDatabaseReadTransaction *transaction) {
 | 
| 1696 | return [OTRImages circleWarningWithColor:[OTRColors warnColor]]; | 
| 1697 | }]; | 
| 1698 | } | 
| 1699 | return _warningAvatarImage; | 
| 1700 | } | 
| 1701 |  | 
| 1702 | - (JSQMessagesAvatarImage *)accountAvatarImage | 
| 1703 | {
 | 
| 1704 |     if (_accountAvatarImage == nil) {
 | 
| 1705 |         _accountAvatarImage = [self createAvatarImage:^(YapDatabaseReadTransaction *transaction) {
 | 
| 1706 | return [[self accountWithTransaction:transaction] avatarImage]; | 
| 1707 | }]; | 
| 1708 | } | 
| 1709 | return _accountAvatarImage; | 
| 1710 | } | 
| 1711 |  | 
| 1712 | - (JSQMessagesAvatarImage *)buddyAvatarImage | 
| 1713 | {
 | 
| 1714 |     if (_buddyAvatarImage == nil) {
 | 
| 1715 |         _buddyAvatarImage = [self createAvatarImage:^(YapDatabaseReadTransaction *transaction) {
 | 
| 1716 | return [[self buddyWithTransaction:transaction] avatarImage]; | 
| 1717 | }]; | 
| 1718 | } | 
| 1719 | return _buddyAvatarImage; | 
| 1720 | } | 
| 1721 |  | 
| 1722 | ////// Optional ////// | 
| 1723 |  | 
| 1724 | - (NSAttributedString *)collectionView:(JSQMessagesCollectionView *)collectionView attributedTextForCellTopLabelAtIndexPath:(NSIndexPath *)indexPath | 
| 1725 | {
 | 
| 1726 | NSMutableAttributedString *text = [[NSMutableAttributedString alloc] init]; | 
| 1727 |  | 
| 1728 |     if ([self showDateAtIndexPath:indexPath]) {
 | 
| 1729 | id <OTRMessageProtocol> message = [self messageAtIndexPath:indexPath]; | 
| 1730 | NSDate *date = [message messageDate]; | 
| 1731 |         if (date != nil) {
 | 
| 1732 | [text appendAttributedString: [[JSQMessagesTimestampFormatter sharedFormatter] attributedTimestampForDate:date]]; | 
| 1733 | } | 
| 1734 | } | 
| 1735 |  | 
| 1736 |     if ([self isPushMessageAtIndexPath:indexPath]) {
 | 
| 1737 | JSQMessagesTimestampFormatter *formatter = [JSQMessagesTimestampFormatter sharedFormatter]; | 
| 1738 | NSString *knockString = KNOCK_SENT_STRING(); | 
| 1739 | //Add new line if there is already a date string | 
| 1740 |         if ([text length] > 0) {
 | 
| 1741 | knockString = [@"\n" stringByAppendingString:knockString]; | 
| 1742 | } | 
| 1743 | [text appendAttributedString:[[NSAttributedString alloc] initWithString:knockString attributes:formatter.dateTextAttributes]]; | 
| 1744 | } | 
| 1745 |  | 
| 1746 | return text; | 
| 1747 | } | 
| 1748 |  | 
| 1749 |  | 
| 1750 | - (NSAttributedString *)collectionView:(JSQMessagesCollectionView *)collectionView attributedTextForMessageBubbleTopLabelAtIndexPath:(NSIndexPath *)indexPath | 
| 1751 | {
 | 
| 1752 |     if ([self showSenderDisplayNameAtIndexPath:indexPath]) {
 | 
| 1753 | id<OTRMessageProtocol,JSQMessageData> message = [self messageAtIndexPath:indexPath]; | 
| 1754 |  | 
| 1755 | __block NSString *displayName = nil; | 
| 1756 |         if ([message isKindOfClass:[OTRXMPPRoomMessage class]]) {
 | 
| 1757 | OTRXMPPRoomMessage *roomMessage = (OTRXMPPRoomMessage *)message; | 
| 1758 |             [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
 | 
| 1759 | OTRXMPPRoomOccupant *occupant = [OTRXMPPRoomOccupant occupantWithJid:[XMPPJID jidWithString:roomMessage.senderJID] realJID:[XMPPJID jidWithString:roomMessage.senderJID] roomJID:[XMPPJID jidWithString:roomMessage.roomJID] accountId:[self accountWithTransaction:transaction].uniqueId createIfNeeded:NO transaction:transaction]; | 
| 1760 |                 if (occupant) {
 | 
| 1761 | OTRXMPPBuddy *buddy = [occupant buddyWith:transaction]; | 
| 1762 |                     if (buddy) {
 | 
| 1763 | displayName = [buddy displayName]; | 
| 1764 |                     } else {
 | 
| 1765 | displayName = [[XMPPJID jidWithString:occupant.jid] resource]; | 
| 1766 | } | 
| 1767 | } | 
| 1768 | }]; | 
| 1769 | } | 
| 1770 |         if (!displayName) {
 | 
| 1771 | displayName = [message senderDisplayName]; | 
| 1772 | } | 
| 1773 | return [[NSAttributedString alloc] initWithString:displayName]; | 
| 1774 | } | 
| 1775 |  | 
| 1776 | return nil; | 
| 1777 | } | 
| 1778 |  | 
| 1779 | /** Currently uses clock for queued, and checkmark for delivered. */ | 
| 1780 | - (nullable NSAttributedString*) deliveryStatusStringForMessage:(nonnull id<OTRMessageProtocol>)message {
 | 
| 1781 |     if (!message) { return nil; }
 | 
| 1782 | // Only applies to outgoing messages | 
| 1783 |     if ([message isMessageIncoming]) {
 | 
| 1784 | return nil; | 
| 1785 | } | 
| 1786 | NSString *deliveryStatusString = nil; | 
| 1787 |     if(message.isMessageSent == NO && ![message messageMediaItemKey]) {
 | 
| 1788 | // Waiting to send message. This message is in the queue. | 
| 1789 | deliveryStatusString = [NSString fa_stringForFontAwesomeIcon:FAClockO]; | 
| 1790 |     } else if (message.isMessageDelivered){
 | 
| 1791 | deliveryStatusString = [NSString stringWithFormat:@"%@ ",[NSString fa_stringForFontAwesomeIcon:FACheck]]; | 
| 1792 | } | 
| 1793 |     if (deliveryStatusString != nil) {
 | 
| 1794 | UIFont *font = [UIFont fontWithName:kFontAwesomeFont size:12]; | 
| 1795 |         if (!font) {
 | 
| 1796 | font = [UIFont systemFontOfSize:12]; | 
| 1797 | } | 
| 1798 |         return [[NSAttributedString alloc] initWithString:deliveryStatusString attributes:@{NSFontAttributeName: font}];
 | 
| 1799 | } | 
| 1800 | return nil; | 
| 1801 | } | 
| 1802 |  | 
| 1803 | - (nullable NSAttributedString *) encryptionStatusStringForMessage:(nonnull id<OTRMessageProtocol>)message {
 | 
| 1804 | NSString *lockString = nil; | 
| 1805 |     if (message.messageSecurity == OTRMessageTransportSecurityOTR) {
 | 
| 1806 | lockString = [NSString stringWithFormat:@"%@ OTR ",[NSString fa_stringForFontAwesomeIcon:FALock]]; | 
| 1807 |     } else if (message.messageSecurity == OTRMessageTransportSecurityOMEMO) {
 | 
| 1808 | lockString = [NSString stringWithFormat:@"%@ OMEMO ",[NSString fa_stringForFontAwesomeIcon:FALock]]; | 
| 1809 | } | 
| 1810 |     else {
 | 
| 1811 | lockString = [NSString fa_stringForFontAwesomeIcon:FAUnlock]; | 
| 1812 | } | 
| 1813 | UIFont *font = [UIFont fontWithName:kFontAwesomeFont size:12]; | 
| 1814 |     if (!font) {
 | 
| 1815 | font = [UIFont systemFontOfSize:12]; | 
| 1816 | } | 
| 1817 |     return [[NSAttributedString alloc] initWithString:lockString attributes:@{NSFontAttributeName: font}];
 | 
| 1818 | } | 
| 1819 |  | 
| 1820 |  | 
| 1821 | - (NSAttributedString *)collectionView:(JSQMessagesCollectionView *)collectionView attributedTextForCellBottomLabelAtIndexPath:(NSIndexPath *)indexPath | 
| 1822 | {
 | 
| 1823 | id <OTRMessageProtocol> message = [self messageAtIndexPath:indexPath]; | 
| 1824 |     if (!message) {
 | 
| 1825 | return [[NSAttributedString alloc] initWithString:@""]; | 
| 1826 | } | 
| 1827 |  | 
| 1828 | UIFont *font = [UIFont fontWithName:kFontAwesomeFont size:12]; | 
| 1829 |     if (!font) {
 | 
| 1830 | font = [UIFont systemFontOfSize:12]; | 
| 1831 | } | 
| 1832 |     NSDictionary *iconAttributes = @{NSFontAttributeName: font};
 | 
| 1833 | NSDictionary *lockAttributes = [iconAttributes copy]; | 
| 1834 |  | 
| 1835 | ////// Lock Icon ////// | 
| 1836 | NSAttributedString *lockString = [self encryptionStatusStringForMessage:message]; | 
| 1837 |     if (!lockString) {
 | 
| 1838 | lockString = [[NSAttributedString alloc] initWithString:@""]; | 
| 1839 | } | 
| 1840 | NSMutableAttributedString *attributedString = [lockString mutableCopy]; | 
| 1841 |  | 
| 1842 | BOOL trusted = YES; | 
| 1843 |     if([message isKindOfClass:[OTRBaseMessage class]]) {
 | 
| 1844 | trusted = [self isMessageTrusted:message]; | 
| 1845 | }; | 
| 1846 |  | 
| 1847 |     if (!trusted) {
 | 
| 1848 | NSMutableDictionary *mutableCopy = [lockAttributes mutableCopy]; | 
| 1849 | [mutableCopy setObject:[UIColor redColor] forKey:NSForegroundColorAttributeName]; | 
| 1850 | lockAttributes = mutableCopy; | 
| 1851 | } | 
| 1852 |  | 
| 1853 | NSAttributedString *deliveryString = [self deliveryStatusStringForMessage:message]; | 
| 1854 |     if (deliveryString) {
 | 
| 1855 | [attributedString appendAttributedString:deliveryString]; | 
| 1856 | } | 
| 1857 |  | 
| 1858 |     if([[message messageMediaItemKey] length] > 0) {
 | 
| 1859 |  | 
| 1860 | __block OTRMediaItem *mediaItem = nil; | 
| 1861 | //Get the media item | 
| 1862 |         [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
 | 
| 1863 | mediaItem = [OTRMediaItem fetchObjectWithUniqueID:[message messageMediaItemKey] transaction:transaction]; | 
| 1864 | }]; | 
| 1865 |         if (!mediaItem) {
 | 
| 1866 | return attributedString; | 
| 1867 | } | 
| 1868 |  | 
| 1869 | float percentProgress = mediaItem.transferProgress * 100; | 
| 1870 |  | 
| 1871 | NSString *progressString = nil; | 
| 1872 | NSUInteger insertIndex = 0; | 
| 1873 |  | 
| 1874 |         if (mediaItem.isIncoming && mediaItem.transferProgress < 1) {
 | 
| 1875 |             if (message.messageError) {
 | 
| 1876 | progressString = [NSString stringWithFormat:@"%@ ",WAITING_STRING()]; | 
| 1877 |             } else {
 | 
| 1878 | progressString = [NSString stringWithFormat:@" %@ %.0f%%",INCOMING_STRING(),percentProgress]; | 
| 1879 | } | 
| 1880 | insertIndex = [attributedString length]; | 
| 1881 |         } else if (!mediaItem.isIncoming && mediaItem.transferProgress < 1) {
 | 
| 1882 |             if(percentProgress > 0) {
 | 
| 1883 | progressString = [NSString stringWithFormat:@"%@ %.0f%% ",SENDING_STRING(),percentProgress]; | 
| 1884 |             } else {
 | 
| 1885 | progressString = [NSString stringWithFormat:@"%@ ",WAITING_STRING()]; | 
| 1886 | } | 
| 1887 | } | 
| 1888 |  | 
| 1889 |         if ([progressString length]) {
 | 
| 1890 | UIFont *font = [UIFont systemFontOfSize:12]; | 
| 1891 |             [attributedString insertAttributedString:[[NSAttributedString alloc] initWithString:progressString attributes:@{NSFontAttributeName: font}] atIndex:insertIndex];
 | 
| 1892 | } | 
| 1893 | } | 
| 1894 |  | 
| 1895 | return attributedString; | 
| 1896 | } | 
| 1897 |  | 
| 1898 |  | 
| 1899 | #pragma - mark JSQMessagesCollectionViewDelegateFlowLayout Methods | 
| 1900 |  | 
| 1901 | - (CGFloat)collectionView:(JSQMessagesCollectionView *)collectionView | 
| 1902 | layout:(JSQMessagesCollectionViewFlowLayout *)collectionViewLayout | 
| 1903 | heightForCellTopLabelAtIndexPath:(NSIndexPath *)indexPath | 
| 1904 | {
 | 
| 1905 | CGFloat height = 0.0f; | 
| 1906 |     if ([self showDateAtIndexPath:indexPath]) {
 | 
| 1907 | height += kJSQMessagesCollectionViewCellLabelHeightDefault; | 
| 1908 | } | 
| 1909 |  | 
| 1910 |     if ([self isPushMessageAtIndexPath:indexPath]) {
 | 
| 1911 | height += kJSQMessagesCollectionViewCellLabelHeightDefault; | 
| 1912 | } | 
| 1913 | return height; | 
| 1914 | } | 
| 1915 |  | 
| 1916 | - (CGFloat)collectionView:(JSQMessagesCollectionView *)collectionView | 
| 1917 | layout:(JSQMessagesCollectionViewFlowLayout *)collectionViewLayout | 
| 1918 | heightForMessageBubbleTopLabelAtIndexPath:(NSIndexPath *)indexPath | 
| 1919 | {
 | 
| 1920 |     if ([self showSenderDisplayNameAtIndexPath:indexPath]) {
 | 
| 1921 | return kJSQMessagesCollectionViewCellLabelHeightDefault; | 
| 1922 | } | 
| 1923 | return 0.0f; | 
| 1924 | } | 
| 1925 |  | 
| 1926 | - (CGFloat)collectionView:(JSQMessagesCollectionView *)collectionView | 
| 1927 | layout:(JSQMessagesCollectionViewFlowLayout *)collectionViewLayout | 
| 1928 | heightForCellBottomLabelAtIndexPath:(NSIndexPath *)indexPath | 
| 1929 | {
 | 
| 1930 | CGFloat height = kJSQMessagesCollectionViewCellLabelHeightDefault; | 
| 1931 |     if ([self isPushMessageAtIndexPath:indexPath]) {
 | 
| 1932 | height = 0.0f; | 
| 1933 | } | 
| 1934 | return height; | 
| 1935 | } | 
| 1936 |  | 
| 1937 | - (void)deleteMessageAtIndexPath:(NSIndexPath *)indexPath | 
| 1938 | {
 | 
| 1939 | __block id <OTRMessageProtocol,JSQMessageData> message = [self messageAtIndexPath:indexPath]; | 
| 1940 | __weak __typeof__(self) weakSelf = self; | 
| 1941 |     [self.readWriteDatabaseConnection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
 | 
| 1942 | __typeof__(self) strongSelf = weakSelf; | 
| 1943 | [transaction removeObjectForKey:[message messageKey] inCollection:[message messageCollection]]; | 
| 1944 | //Update Last message date for sorting and grouping | 
| 1945 | OTRBuddy *buddy = [[strongSelf buddyWithTransaction:transaction] copy]; | 
| 1946 | buddy.lastMessageId = nil; | 
| 1947 | [buddy saveWithTransaction:transaction]; | 
| 1948 | }]; | 
| 1949 | } | 
| 1950 |  | 
| 1951 | - (void)collectionView:(JSQMessagesCollectionView *)collectionView didTapAvatarImageView:(UIImageView *)avatarImageView atIndexPath:(NSIndexPath *)indexPath | 
| 1952 | {
 | 
| 1953 | id <OTRMessageProtocol,JSQMessageData> message = [self messageAtIndexPath:indexPath]; | 
| 1954 | [self didTapAvatar:message sender:avatarImageView]; | 
| 1955 | } | 
| 1956 |  | 
| 1957 | - (void)collectionView:(JSQMessagesCollectionView *)collectionView didTapMessageBubbleAtIndexPath:(NSIndexPath *)indexPath | 
| 1958 | {
 | 
| 1959 | id <OTRMessageProtocol,JSQMessageData> message = [self messageAtIndexPath:indexPath]; | 
| 1960 |     if (!message.isMediaMessage) {
 | 
| 1961 | return; | 
| 1962 | } | 
| 1963 | __block OTRMediaItem *item = nil; | 
| 1964 |     [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
 | 
| 1965 | item = [OTRMediaItem mediaItemForMessage:message transaction:transaction]; | 
| 1966 | }]; | 
| 1967 |     if (!item) { return; }
 | 
| 1968 |     if (item.transferProgress != 1 && item.isIncoming) {
 | 
| 1969 | return; | 
| 1970 | } | 
| 1971 |  | 
| 1972 |     if ([item isKindOfClass:[OTRImageItem class]]) {
 | 
| 1973 | [self showImage:(OTRImageItem *)item fromCollectionView:collectionView atIndexPath:indexPath]; | 
| 1974 | } | 
| 1975 |     else if ([item isKindOfClass:[OTRVideoItem class]]) {
 | 
| 1976 | [self showVideo:(OTRVideoItem *)item fromCollectionView:collectionView atIndexPath:indexPath]; | 
| 1977 | } | 
| 1978 |     else if ([item isKindOfClass:[OTRAudioItem class]]) {
 | 
| 1979 | [self playOrPauseAudio:(OTRAudioItem *)item fromCollectionView:collectionView atIndexPath:indexPath]; | 
| 1980 |     } else if ([message conformsToProtocol:@protocol(OTRDownloadMessage)]) {
 | 
| 1981 | id<OTRDownloadMessage> download = (id<OTRDownloadMessage>)message; | 
| 1982 | // Janky hack to open URL for now | 
| 1983 | NSArray<UIAlertAction*> *actions = [UIAlertAction actionsForMediaMessage:download sourceView:self.view viewController:self]; | 
| 1984 | UIAlertController *alert = [UIAlertController alertControllerWithTitle:message.text message:nil preferredStyle:UIAlertControllerStyleActionSheet]; | 
| 1985 |         [actions enumerateObjectsUsingBlock:^(UIAlertAction * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
 | 
| 1986 | [alert addAction:obj]; | 
| 1987 | }]; | 
| 1988 | [alert addAction:[self cancleAction]]; | 
| 1989 |  | 
| 1990 | // Get the anchor | 
| 1991 | alert.popoverPresentationController.sourceView = self.view; | 
| 1992 | alert.popoverPresentationController.sourceRect = self.view.bounds; | 
| 1993 | UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath]; | 
| 1994 |         if ([cell isKindOfClass:[JSQMessagesCollectionViewCell class]]) {
 | 
| 1995 | UIView *cellContainterView = ((JSQMessagesCollectionViewCell *)cell).messageBubbleContainerView; | 
| 1996 | alert.popoverPresentationController.sourceRect = cellContainterView.bounds; | 
| 1997 | alert.popoverPresentationController.sourceView = cellContainterView; | 
| 1998 | } | 
| 1999 |  | 
| 2000 | [self presentViewController:alert animated:YES completion:nil]; | 
| 2001 | } | 
| 2002 | } | 
| 2003 |  | 
| 2004 | #pragma - mark database view delegate | 
| 2005 |  | 
| 2006 | - (void)didSetupMappings:(OTRYapViewHandler *)handler | 
| 2007 | {
 | 
| 2008 | // The databse view is setup now so refresh from there | 
| 2009 | [self updateViewWithKey:self.threadKey collection:self.threadCollection]; | 
| 2010 | [self updateRangeOptions:YES]; | 
| 2011 | [self.collectionView reloadData]; | 
| 2012 | } | 
| 2013 |  | 
| 2014 | - (void)didReceiveChanges:(OTRYapViewHandler *)handler key:(NSString *)key collection:(NSString *)collection | 
| 2015 | {
 | 
| 2016 | [self updateViewWithKey:key collection:collection]; | 
| 2017 | } | 
| 2018 |  | 
| 2019 | - (void)didReceiveChanges:(OTRYapViewHandler *)handler sectionChanges:(NSArray<YapDatabaseViewSectionChange *> *)sectionChanges rowChanges:(NSArray<YapDatabaseViewRowChange *> *)rowChanges | 
| 2020 | {
 | 
| 2021 |     if (!rowChanges.count) {
 | 
| 2022 | return; | 
| 2023 | } | 
| 2024 |  | 
| 2025 | // Important to clear our "one message cache" here, since things may have changed. | 
| 2026 | self.currentIndexPath = nil; | 
| 2027 |  | 
| 2028 | NSUInteger collectionViewNumberOfItems = [self.collectionView numberOfItemsInSection:0]; | 
| 2029 | NSUInteger numberMappingsItems = [self.viewHandler.mappings numberOfItemsInSection:0]; | 
| 2030 |  | 
| 2031 |     [self.collectionView performBatchUpdates:^{
 | 
| 2032 |  | 
| 2033 | for (YapDatabaseViewRowChange *rowChange in rowChanges) | 
| 2034 |         {
 | 
| 2035 | switch (rowChange.type) | 
| 2036 |             {
 | 
| 2037 | case YapDatabaseViewChangeDelete : | 
| 2038 |                 {
 | 
| 2039 | [self.collectionView deleteItemsAtIndexPaths:@[rowChange.indexPath]]; | 
| 2040 | break; | 
| 2041 | } | 
| 2042 | case YapDatabaseViewChangeInsert : | 
| 2043 |                 {
 | 
| 2044 | [self.collectionView insertItemsAtIndexPaths:@[ rowChange.newIndexPath ]]; | 
| 2045 | break; | 
| 2046 | } | 
| 2047 | case YapDatabaseViewChangeMove : | 
| 2048 |                 {
 | 
| 2049 | [self.collectionView moveItemAtIndexPath:rowChange.indexPath toIndexPath:rowChange.newIndexPath]; | 
| 2050 | break; | 
| 2051 | } | 
| 2052 | case YapDatabaseViewChangeUpdate : | 
| 2053 |                 {
 | 
| 2054 | // Update could be e.g. when we are done auto-loading a link. We | 
| 2055 | // need to reset the stored size of this item, so the image/message | 
| 2056 | // will get the correct bubble height. | 
| 2057 | id <JSQMessageData> message = [self messageAtIndexPath:rowChange.indexPath]; | 
| 2058 | [self.collectionView.collectionViewLayout.bubbleSizeCalculator resetBubbleSizeCacheForMessageData:message]; | 
| 2059 | [self.messageSizeCache removeObjectForKey:@(message.messageHash)]; | 
| 2060 | [self.collectionView reloadItemsAtIndexPaths:@[ rowChange.indexPath]]; | 
| 2061 | break; | 
| 2062 | } | 
| 2063 | } | 
| 2064 | } | 
| 2065 |     } completion:^(BOOL finished){
 | 
| 2066 |         if(numberMappingsItems > collectionViewNumberOfItems && numberMappingsItems > 0) {
 | 
| 2067 | //Inserted new item, probably at the end | 
| 2068 | //Get last message and test if isIncoming | 
| 2069 | NSIndexPath *lastMessageIndexPath = [NSIndexPath indexPathForRow:numberMappingsItems - 1 inSection:0]; | 
| 2070 | id <OTRMessageProtocol>lastMessage = [self messageAtIndexPath:lastMessageIndexPath]; | 
| 2071 |             if ([lastMessage isMessageIncoming]) {
 | 
| 2072 | [self finishReceivingMessage]; | 
| 2073 |             } else {
 | 
| 2074 | // We can't use finishSendingMessage here because it might | 
| 2075 | // accidentally clear out unsent message text | 
| 2076 | [self.collectionView.collectionViewLayout invalidateLayoutWithContext:[JSQMessagesCollectionViewFlowLayoutInvalidationContext context]]; | 
| 2077 | [self scrollToBottomAnimated:YES]; | 
| 2078 | } | 
| 2079 |         } else {
 | 
| 2080 | [self.collectionView.collectionViewLayout invalidateLayoutWithContext:[JSQMessagesCollectionViewFlowLayoutInvalidationContext context]]; | 
| 2081 | } | 
| 2082 | }]; | 
| 2083 | } | 
| 2084 |  | 
| 2085 | #pragma - mark UITextViewDelegateMethods | 
| 2086 |  | 
| 2087 | - (BOOL)textView:(UITextView *)textView shouldInteractWithURL:(NSURL *)URL inRange:(NSRange)characterRange | 
| 2088 | {
 | 
| 2089 |     if ([URL otr_isInviteLink]) {
 | 
| 2090 | NSUserActivity *activity = [[NSUserActivity alloc] initWithActivityType:NSUserActivityTypeBrowsingWeb]; | 
| 2091 | activity.webpageURL = URL; | 
| 2092 |         [[OTRAppDelegate appDelegate] application:[UIApplication sharedApplication] continueUserActivity:activity restorationHandler:^(NSArray * _Nullable restorableObjects) {
 | 
| 2093 | // TODO: restore stuff | 
| 2094 | }]; | 
| 2095 | return NO; | 
| 2096 | } | 
| 2097 |  | 
| 2098 | UIActivityViewController *activityViewController = [UIActivityViewController otr_linkActivityViewControllerWithURLs:@[URL]]; | 
| 2099 |  | 
| 2100 |     if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"8.0")) {
 | 
| 2101 | activityViewController.popoverPresentationController.sourceView = textView; | 
| 2102 | activityViewController.popoverPresentationController.sourceRect = textView.bounds; | 
| 2103 | } | 
| 2104 |  | 
| 2105 | [self presentViewController:activityViewController animated:YES completion:nil]; | 
| 2106 | return NO; | 
| 2107 | } | 
| 2108 |  | 
| 2109 | - (BOOL)textFieldShouldBeginEditing:(UITextField *)textField{
 | 
| 2110 | return NO; | 
| 2111 | } | 
| 2112 |  | 
| 2113 | - (void)viewWillLayoutSubviews {
 | 
| 2114 | self.currentIndexPath = nil; | 
| 2115 | [super viewWillLayoutSubviews]; | 
| 2116 | } | 
| 2117 |  | 
| 2118 | - (void)viewDidLayoutSubviews {
 | 
| 2119 | [super viewDidLayoutSubviews]; | 
| 2120 | [self layoutJIDForwardingHeader]; | 
| 2121 | } | 
| 2122 |  | 
| 2123 | #pragma - mark Buddy Migration methods | 
| 2124 |  | 
| 2125 | - (nullable XMPPJID *)getForwardingJIDForBuddy:(OTRXMPPBuddy *)xmppBuddy {
 | 
| 2126 | XMPPJID *ret = nil; | 
| 2127 |     if (xmppBuddy != nil && xmppBuddy.vCardTemp != nil) {
 | 
| 2128 | ret = xmppBuddy.vCardTemp.jid; | 
| 2129 | } | 
| 2130 | return ret; | 
| 2131 | } | 
| 2132 |  | 
| 2133 | - (void)layoutJIDForwardingHeader {
 | 
| 2134 |     if (self.jidForwardingHeaderView != nil) {
 | 
| 2135 | [self.jidForwardingHeaderView setNeedsLayout]; | 
| 2136 | [self.jidForwardingHeaderView layoutIfNeeded]; | 
| 2137 | int height = [self.jidForwardingHeaderView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height + 1; | 
| 2138 | self.jidForwardingHeaderView.frame = CGRectMake(0, self.topLayoutGuide.length, self.view.frame.size.width, height); | 
| 2139 | [self.view bringSubviewToFront:self.jidForwardingHeaderView]; | 
| 2140 | self.topContentAdditionalInset = height; | 
| 2141 | } | 
| 2142 | } | 
| 2143 |  | 
| 2144 | - (void)updateJIDForwardingHeader {
 | 
| 2145 |  | 
| 2146 | __block id<OTRThreadOwner> thread = nil; | 
| 2147 |     [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
 | 
| 2148 | thread = [self threadObjectWithTransaction:transaction]; | 
| 2149 | }]; | 
| 2150 | OTRXMPPBuddy *buddy = nil; | 
| 2151 |     if ([thread isKindOfClass:[OTRXMPPBuddy class]]) {
 | 
| 2152 | buddy = (OTRXMPPBuddy*)thread; | 
| 2153 | } | 
| 2154 |  | 
| 2155 | // If we have a buddy with vcard JID set to something else than the username, show a | 
| 2156 | // "buddy has moved" warning to allow the user to start a chat with that JID instead. | 
| 2157 | BOOL showHeader = NO; | 
| 2158 | XMPPJID *forwardingJid = [self getForwardingJIDForBuddy:buddy]; | 
| 2159 |     if (forwardingJid != nil && ![forwardingJid isEqualToJID:buddy.bareJID options:XMPPJIDCompareBare]) {
 | 
| 2160 | showHeader = YES; | 
| 2161 | } | 
| 2162 |  | 
| 2163 |     if (showHeader) {
 | 
| 2164 | [self showJIDForwardingHeaderWithNewJID:forwardingJid]; | 
| 2165 |     } else if (!showHeader && self.jidForwardingHeaderView != nil) {
 | 
| 2166 | self.topContentAdditionalInset = 0; | 
| 2167 | [self.jidForwardingHeaderView removeFromSuperview]; | 
| 2168 | self.jidForwardingHeaderView = nil; | 
| 2169 | } | 
| 2170 | } | 
| 2171 |  | 
| 2172 | - (void)showJIDForwardingHeaderWithNewJID:(XMPPJID *)newJid {
 | 
| 2173 |     if (self.jidForwardingHeaderView == nil) {
 | 
| 2174 | UINib *nib = [UINib nibWithNibName:@"MigratedBuddyHeaderView" bundle:OTRAssets.resourcesBundle]; | 
| 2175 | MigratedBuddyHeaderView *header = (MigratedBuddyHeaderView*)[nib instantiateWithOwner:self options:nil][0]; | 
| 2176 | [header setForwardingJID:newJid]; | 
| 2177 | [header.titleLabel setText:MIGRATED_BUDDY_STRING()]; | 
| 2178 | [header.descriptionLabel setText:MIGRATED_BUDDY_INFO_STRING()]; | 
| 2179 | [header.switchButton setTitle:MIGRATED_BUDDY_SWITCH() forState:UIControlStateNormal]; | 
| 2180 | [header.ignoreButton setTitle:MIGRATED_BUDDY_IGNORE() forState:UIControlStateNormal]; | 
| 2181 | [header setBackgroundColor:UIColor.whiteColor]; | 
| 2182 | [self.view addSubview:header]; | 
| 2183 | [self.view bringSubviewToFront:header]; | 
| 2184 | self.jidForwardingHeaderView = header; | 
| 2185 | [self.view setNeedsLayout]; | 
| 2186 | } | 
| 2187 | } | 
| 2188 |  | 
| 2189 | - (IBAction)didPressMigratedIgnore {
 | 
| 2190 |     if (self.jidForwardingHeaderView != nil) {
 | 
| 2191 | self.jidForwardingHeaderView.hidden = YES; | 
| 2192 | self.topContentAdditionalInset = 0; | 
| 2193 | } | 
| 2194 | } | 
| 2195 |  | 
| 2196 | - (IBAction)didPressMigratedSwitch {
 | 
| 2197 |     if (self.jidForwardingHeaderView != nil) {
 | 
| 2198 | self.jidForwardingHeaderView.hidden = YES; | 
| 2199 | self.topContentAdditionalInset = 0; | 
| 2200 | } | 
| 2201 |  | 
| 2202 | __block OTRXMPPBuddy *buddy = nil; | 
| 2203 |     [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
 | 
| 2204 | buddy = (OTRXMPPBuddy*)[self buddyWithTransaction:transaction]; | 
| 2205 | }]; | 
| 2206 |  | 
| 2207 | XMPPJID *forwardingJid = [self getForwardingJIDForBuddy:buddy]; | 
| 2208 |     if (forwardingJid != nil) {
 | 
| 2209 | // Try to find buddy | 
| 2210 | // | 
| 2211 |         [[OTRDatabaseManager sharedInstance].readWriteDatabaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
 | 
| 2212 | OTRAccount *account = [self accountWithTransaction:transaction]; | 
| 2213 | OTRXMPPBuddy *buddy = [OTRXMPPBuddy fetchBuddyWithJid:forwardingJid accountUniqueId:account.uniqueId transaction:transaction]; | 
| 2214 |             if (!buddy) {
 | 
| 2215 | buddy = [[OTRXMPPBuddy alloc] init]; | 
| 2216 | buddy.accountUniqueId = account.uniqueId; | 
| 2217 | buddy.username = forwardingJid.bare; | 
| 2218 | [buddy saveWithTransaction:transaction]; | 
| 2219 | id<OTRProtocol> proto = [[OTRProtocolManager sharedInstance] protocolForAccount:account]; | 
| 2220 |                 if (proto != nil) {
 | 
| 2221 | [proto addBuddy:buddy]; | 
| 2222 | } | 
| 2223 | } | 
| 2224 | [self setThreadKey:buddy.uniqueId collection:[OTRBuddy collection]]; | 
| 2225 | }]; | 
| 2226 | } | 
| 2227 | } | 
| 2228 |  | 
| 2229 | #pragma - mark Group chat support | 
| 2230 |  | 
| 2231 | - (void)setupWithBuddies:(NSArray<NSString *> *)buddies accountId:(NSString *)accountId name:(NSString *)name | 
| 2232 | {
 | 
| 2233 | __block OTRXMPPAccount *account = nil; | 
| 2234 |     [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
 | 
| 2235 | account = [OTRXMPPAccount fetchObjectWithUniqueID:accountId transaction:transaction]; | 
| 2236 | }]; | 
| 2237 | OTRXMPPManager *xmppManager = (OTRXMPPManager *)[[OTRProtocolManager sharedInstance] protocolForAccount:account]; | 
| 2238 | NSString *service = [xmppManager.roomManager.conferenceServicesJID firstObject]; | 
| 2239 |     if (service.length > 0) {
 | 
| 2240 | NSString *roomName = [NSUUID UUID].UUIDString; | 
| 2241 | XMPPJID *roomJID = [XMPPJID jidWithString:[NSString stringWithFormat:@"%@@%@",roomName,service]]; | 
| 2242 | self.threadKey = [xmppManager.roomManager startGroupChatWithBuddies:buddies roomJID:roomJID nickname:account.displayName subject:name]; | 
| 2243 | [self setThreadKey:self.threadKey collection:[OTRXMPPRoom collection]]; | 
| 2244 |     } else {
 | 
| 2245 | DDLogError(@"No conference server for account: %@", account.username); | 
| 2246 | } | 
| 2247 | } | 
| 2248 |  | 
| 2249 | #pragma - mark OTRRoomOccupantsViewControllerDelegate | 
| 2250 |  | 
| 2251 | - (void)didLeaveRoom:(OTRRoomOccupantsViewController *)roomOccupantsViewController {
 | 
| 2252 | __block OTRXMPPRoom *room = nil; | 
| 2253 |     [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
 | 
| 2254 | room = [self roomWithTransaction:transaction]; | 
| 2255 | }]; | 
| 2256 |     if (room) {
 | 
| 2257 | [self setThreadKey:nil collection:nil]; | 
| 2258 |         [self.readWriteDatabaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) {
 | 
| 2259 | [room removeWithTransaction:transaction]; | 
| 2260 | }]; | 
| 2261 | } | 
| 2262 | [self.navigationController popViewControllerAnimated:NO]; | 
| 2263 |     if ([[self.navigationController viewControllers] count] > 1) {
 | 
| 2264 | [self.navigationController popViewControllerAnimated:YES]; | 
| 2265 |     } else {
 | 
| 2266 | [self.navigationController.navigationController popViewControllerAnimated:YES]; | 
| 2267 | } | 
| 2268 |  | 
| 2269 | } | 
| 2270 |  | 
| 2271 | - (void)didArchiveRoom:(OTRRoomOccupantsViewController *)roomOccupantsViewController {
 | 
| 2272 | __block OTRXMPPRoom *room = nil; | 
| 2273 |     [self.readOnlyDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
 | 
| 2274 | room = [self roomWithTransaction:transaction]; | 
| 2275 | }]; | 
| 2276 |     if (room) {
 | 
| 2277 | [self setThreadKey:nil collection:nil]; | 
| 2278 |         [self.readWriteDatabaseConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) {
 | 
| 2279 | room.isArchived = YES; | 
| 2280 | [room saveWithTransaction:transaction]; | 
| 2281 | }]; | 
| 2282 | } | 
| 2283 | [self.navigationController popViewControllerAnimated:NO]; | 
| 2284 |     if ([[self.navigationController viewControllers] count] > 1) {
 | 
| 2285 | [self.navigationController popViewControllerAnimated:YES]; | 
| 2286 |     } else {
 | 
| 2287 | [self.navigationController.navigationController popViewControllerAnimated:YES]; | 
| 2288 | } | 
| 2289 | } | 
| 2290 | @end |